mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-27 05:13:47 +02:00
575 lines
16 KiB
ObjectPascal
575 lines
16 KiB
ObjectPascal
{
|
|
|
|
*****************************************************************************
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
*****************************************************************************
|
|
|
|
Authors: Alexander Klenin
|
|
|
|
}
|
|
unit TAFitUtils;
|
|
|
|
{$MODE ObjFPC}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils, Classes, TAChartUtils, TAFitLib;
|
|
|
|
type
|
|
TFitEquation = (
|
|
fePolynomial, // y = b0 + b1*x + b2*x^2 + ... bn*x^n
|
|
feLinear, // y = a + b*x
|
|
feExp, // y = a * exp(b * x)
|
|
fePower, // y = a * x^b
|
|
feCustom // y = b0 + b1*F1(x) + b2*F2(x) + ... bn*Fn(x),
|
|
// Fi(x) = custom "fit base function" provided by calling SetFitBasisFunc() method
|
|
);
|
|
|
|
IFitEquationText = interface
|
|
function BasisFuncs(const ATexts: array of string): IFitEquationText;
|
|
function DecimalSeparator(AValue: Char): IFitEquationText;
|
|
function Equation(AEquation: TFitEquation): IFitEquationText;
|
|
function X(AText: String): IFitEquationText;
|
|
function Y(AText: String): IFitEquationText;
|
|
function NumFormat(AFormat: String): IFitEquationText;
|
|
function NumFormats(const AFormats: array of String): IFitEquationText;
|
|
function Params(const AParams: array of Double): IFitEquationText;
|
|
function TextFormat(AFormat: TChartTextFormat): IFitEquationText;
|
|
function Get: String;
|
|
end;
|
|
|
|
TFitEmptyEquationText = class(TInterfacedObject, IFitEquationText)
|
|
public
|
|
function BasisFuncs(const ATexts: array of string): IFitEquationText;
|
|
function DecimalSeparator(AValue: Char): IFitEquationText;
|
|
function Equation(AEquation: TFitEquation): IFitEquationText;
|
|
function X(AText: String): IFitEquationText;
|
|
function Y(AText: String): IFitEquationText;
|
|
function NumFormat(AFormat: String): IFitEquationText;
|
|
function NumFormats(const AFormats: array of String): IFitEquationText;
|
|
function Params(const AParams: array of Double): IFitEquationText;
|
|
function TextFormat(AFormat: TChartTextFormat): IFitEquationText;
|
|
function Get: String;
|
|
end;
|
|
|
|
TFitEquationText = class(TInterfacedObject, IFitEquationText)
|
|
strict private
|
|
FBasisFunc: array of string;
|
|
FDecSep: Char;
|
|
FEquation: TFitEquation;
|
|
FX: String;
|
|
FY: String;
|
|
FNumFormat: String;
|
|
FNumFormats: array of String;
|
|
FParams: array of Double;
|
|
FTextFormat: TChartTextFormat;
|
|
function GetNumFormat(AIndex: Integer): String;
|
|
public
|
|
constructor Create;
|
|
function BasisFuncs(const ATexts: array of string): IFitEquationText;
|
|
function DecimalSeparator(AValue: Char): IFitEquationText;
|
|
function Equation(AEquation: TFitEquation): IFitEquationText;
|
|
function X(AText: String): IFitEquationText;
|
|
function Y(AText: String): IFitEquationText;
|
|
function NumFormat(AFormat: String): IFitEquationText;
|
|
function NumFormats(const AFormats: array of String): IFitEquationText;
|
|
function Params(const AParams: array of Double): IFitEquationText;
|
|
function TextFormat(AFormat: TChartTextFormat): IFitEquationText;
|
|
function Get: String;
|
|
end;
|
|
|
|
TFitStatistics = class(TObject)
|
|
private
|
|
fN: Integer; // number of observations
|
|
fM: Integer; // number of (adjusted) fit parameters (fixed params not included)
|
|
fSST: Double; // total sum of squares (yi - ybar)^2
|
|
fSSR: Double; // regression sum of squares (yhat - ybar)^2
|
|
fSSE: Double; // error sum of squares (yi - yhat)^2
|
|
fAlpha: Double; // significance level for hypothesis tests
|
|
fxbar: Double; // mean x value
|
|
fSSx: Double; // sum of squares (xi - xbar)³
|
|
fVarCovar: array of array of Double;
|
|
fTValue: Double; // t-value
|
|
procedure CalcTValue;
|
|
function GetVarCovar(i, j: Integer): Double;
|
|
procedure SetAlpha(AValue: Double);
|
|
public
|
|
constructor Create(aFitResults: TFitResults; aAlpha: Double = 0.05);
|
|
procedure Report_ANOVA(AText: TStrings; ASeparator: String = ': ';
|
|
ANumFormat: String = '%f'; AExpFormat: String = '%.3e'; NaNStr: String = 'n/a');
|
|
procedure Report_VarCovar(AText: TSTrings; ANumFormat: String = '%12.6f');
|
|
public
|
|
function AdjR2: Double;
|
|
function Chi2: Double;
|
|
function DOF: Integer; // Degrees of freedom
|
|
function F: Double;
|
|
property N: Integer read fN; // Number of data points
|
|
property M: Integer read fM; // Number of fit parameters
|
|
function ReducedChi2: Double;
|
|
function R2: Double;
|
|
function ResidualStdError: Double;
|
|
property Alpha: Double read FAlpha write SetAlpha;
|
|
property SST: Double read fSST;
|
|
property SSR: Double read fSSR;
|
|
property SSE: Double read fSSE;
|
|
property VarCovar[i, j: Integer]: Double read GetVarCovar;
|
|
property xBar: Double read fXBar;
|
|
property SSx: Double read fSSx;
|
|
public
|
|
function Fcrit: Double;
|
|
function pValue: Double;
|
|
property tValue: Double read ftValue;
|
|
end;
|
|
|
|
operator := (AEq: IFitEquationText): String; inline;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math, spe, TAChartStrConsts;
|
|
|
|
operator := (AEq: IFitEquationText): String;
|
|
begin
|
|
Result := AEq.Get;
|
|
end;
|
|
|
|
|
|
{ TFitEmptyEquationText }
|
|
|
|
function TFitEmptyEquationText.BasisFuncs(const ATexts: array of string): IFitEquationText;
|
|
begin
|
|
Unused(ATexts);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.DecimalSeparator(AValue: Char): IFitEquationText;
|
|
begin
|
|
Unused(AValue);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.Equation(
|
|
AEquation: TFitEquation): IFitEquationText;
|
|
begin
|
|
Unused(AEquation);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.Get: String;
|
|
begin
|
|
Result := '';
|
|
end;
|
|
|
|
function TFitEmptyEquationText.NumFormat(AFormat: String): IFitEquationText;
|
|
begin
|
|
Unused(AFormat);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.NumFormats(
|
|
const AFormats: array of String): IFitEquationText;
|
|
begin
|
|
Unused(AFormats);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.Params(
|
|
const AParams: array of Double): IFitEquationText;
|
|
begin
|
|
Unused(AParams);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.TextFormat(AFormat: TChartTextFormat): IFitEquationText;
|
|
begin
|
|
Unused(AFormat);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.X(AText: String): IFitEquationText;
|
|
begin
|
|
Unused(AText);
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEmptyEquationText.Y(AText: String): IFitEquationText;
|
|
begin
|
|
Unused(AText);
|
|
Result := Self;
|
|
end;
|
|
|
|
{ TFitEquationText }
|
|
|
|
constructor TFitEquationText.Create;
|
|
begin
|
|
FX := 'x';
|
|
FY := 'y';
|
|
FNumFormat := '%.9g';
|
|
FDecSep := DefaultFormatSettings.DecimalSeparator;
|
|
end;
|
|
|
|
function TFitEquationText.BasisFuncs(const ATexts: array of string): IFitEquationText;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
SetLength(FBasisFunc, Length(ATexts));
|
|
for i := 0 to High(FBasisFunc) do
|
|
FBasisFunc[i] := ATexts[i];
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.DecimalSeparator(AValue: Char): IFitEquationText;
|
|
begin
|
|
FDecSep := AValue;
|
|
Result := self;
|
|
end;
|
|
|
|
function TFitEquationText.Equation(AEquation: TFitEquation): IFitEquationText;
|
|
begin
|
|
FEquation := AEquation;
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.Get: String;
|
|
const
|
|
TIMES: array[boolean] of string = ('*', '·');
|
|
var
|
|
fs: TFormatSettings;
|
|
res: String;
|
|
|
|
// Returns the function term, e.g. "x^3"
|
|
function FuncTerm(i: Integer): String;
|
|
const
|
|
POWER: array[boolean] of string = ('%s^%d', '%s<sup>%d</sup>');
|
|
begin
|
|
if FEquation in [feCustom] then
|
|
Result := FBasisFunc[i]
|
|
else
|
|
if i = 0 then
|
|
Result := ''
|
|
else
|
|
if i = 1 then
|
|
Result := FX
|
|
else
|
|
Result := Format(POWER[FTextFormat = tfHTML], [FX, i]);
|
|
end;
|
|
|
|
// Creates a product term "value*f(x)".
|
|
// "value*" is omitted if 1. "*f(x)" is omitted if constant.
|
|
function ProductTerm(i: Integer): String;
|
|
var
|
|
fx: String;
|
|
begin
|
|
if FParams[i] = 0.0 then
|
|
exit('');
|
|
fx := FuncTerm(i);
|
|
if abs(FParams[i]) <> 1.0 then begin
|
|
Result := Format(GetNumFormat(i), [FParams[i]], fs);
|
|
if fx <> '' then
|
|
Result := Result + TIMES[FTextFormat = tfHTML] + fx;
|
|
end else
|
|
if FParams[i] = -1.0 then
|
|
Result := '-' + FX
|
|
else
|
|
Result := FX;
|
|
end;
|
|
|
|
// First term in expression: no plus sign
|
|
// Other terms: both plus and minus signs, enclosed by spaces.
|
|
function FixSign(s: String): String;
|
|
begin
|
|
if (s <> '') and (res <> '') then begin
|
|
if s[1] = '-' then begin
|
|
Insert(' ', s, 2);
|
|
s := ' ' + s;
|
|
end else
|
|
s := ' + ' + s;
|
|
end;
|
|
Result := s;
|
|
end;
|
|
|
|
// Creates the constant term for feExp or fePower; includes the multiplication sign.
|
|
function ConstTerm: String;
|
|
begin
|
|
if FParams[0] = 1.0 then
|
|
Result := ''
|
|
else if FParams[0] = -1.0 then
|
|
Result := '-'
|
|
else
|
|
Result := Format(GetNumFormat(0), [FParams[0]], fs) + TIMES[FTextFormat = tfHTML];
|
|
end;
|
|
|
|
// Creates the exponential term, e.g. 'exp(-1.2*x)' or 'e<sup>-1.2·x</sup>'
|
|
function ExpTerm: string;
|
|
const
|
|
EXP: array[boolean] of String = ('exp(%s)', 'e<sup>%s</sup>');
|
|
begin
|
|
Result := Format(EXP[FTextFormat = tfHTML], [ProductTerm(1)]);
|
|
end;
|
|
|
|
// Creates the power term, e.g. 'x^1.2' or 'x<sup>1.2</sup>'
|
|
function PowerTerm: String;
|
|
var
|
|
mask: String;
|
|
begin
|
|
if FTextFormat = tfNormal then
|
|
mask := '%s^' + GetNumFormat(1)
|
|
else
|
|
mask := '%s<sup>' + GetNumFormat(1) + '</sup>';
|
|
Result := Format(mask, [FX, FParams[1]], fs);
|
|
end;
|
|
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if Length(FParams) = 0 then
|
|
exit('');
|
|
|
|
fs := DefaultFormatSettings;
|
|
fs.DecimalSeparator := FDecSep;
|
|
|
|
Result := Format('%s = ', [FY]);
|
|
case FEquation of
|
|
feLinear, fePolynomial, feCustom:
|
|
begin
|
|
res := '';
|
|
for i := 0 to High(FParams) do
|
|
res += FixSign(ProductTerm(i));
|
|
Result += res;
|
|
end;
|
|
feExp:
|
|
Result += ConstTerm + ExpTerm;
|
|
fePower:
|
|
Result += ConstTerm + PowerTerm;
|
|
end;
|
|
end;
|
|
|
|
function TFitEquationText.GetNumFormat(AIndex: Integer): String;
|
|
begin
|
|
if AIndex < Length(FNumFormats) then
|
|
Result := FNumFormats[AIndex]
|
|
else
|
|
Result := FNumFormat;
|
|
end;
|
|
|
|
function TFitEquationText.NumFormat(AFormat: String): IFitEquationText;
|
|
begin
|
|
FNumFormat := AFormat;
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.NumFormats(
|
|
const AFormats: array of String): IFitEquationText;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
SetLength(FNumFormats, Length(AFormats));
|
|
for i := 0 to High(AFormats) do
|
|
FNumFormats[i] := AFormats[i];
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.Params(
|
|
const AParams: array of Double): IFitEquationText;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
SetLength(FParams, Length(AParams));
|
|
for i := 0 to High(AParams) do
|
|
FParams[i] := AParams[i];
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.TextFormat(AFormat: TChartTextFormat): IFitEquationText;
|
|
begin
|
|
FTextFormat := AFormat;
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.X(AText: String): IFitEquationText;
|
|
begin
|
|
FX := AText;
|
|
Result := Self;
|
|
end;
|
|
|
|
function TFitEquationText.Y(AText: String): IFitEquationText;
|
|
begin
|
|
FY := AText;
|
|
Result := Self;
|
|
end;
|
|
|
|
|
|
{ TFitStatistics }
|
|
|
|
constructor TFitStatistics.Create(aFitResults: TFitResults;
|
|
aAlpha: Double = 0.05);
|
|
var
|
|
i, j, L: Integer;
|
|
begin
|
|
fN := aFitResults.N;
|
|
fM := aFitResults.M;
|
|
fSSR := aFitResults.SSR;
|
|
fSSE := aFitResults.SSE;
|
|
fSST := aFitResults.SSR + aFitResults.SSE;
|
|
fAlpha := aAlpha;
|
|
L := Length(aFitResults.CovarianceMatrix);
|
|
SetLength(fVarCovar, L, L);
|
|
for j := 0 to L-1 do
|
|
for i := 0 to L-1 do
|
|
fVarCovar[i, j] := aFitResults.CovarianceMatrix[i, j];
|
|
fXBar := aFitResults.XBar;
|
|
fSSx := aFitResults.SSx;
|
|
CalcTValue;
|
|
end;
|
|
|
|
{ Coefficient of determination, adjusted to number of data points and fit
|
|
parameters. Should be close to 1 ("good" fit). "0" means: "poor" fit }
|
|
function TFitStatistics.AdjR2: Double;
|
|
begin
|
|
if DOF > 0 then
|
|
Result := 1.0 - (1.0 - R2) * (N - 1) / DOF
|
|
else
|
|
Result := NaN;
|
|
end;
|
|
|
|
procedure TFitStatistics.CalcTValue;
|
|
begin
|
|
fTValue := NaN;
|
|
if (fAlpha > 0) and (fN > fM) then
|
|
fTValue := invtdist(fAlpha, fN - fM, 2)
|
|
end;
|
|
|
|
{ Total variance of data values minus calculated values, weighted by
|
|
data error.
|
|
For a "moderately" good fit Chi2 is approximately equal to the degrees of
|
|
freedom (DOF). }
|
|
function TFitStatistics.Chi2: Double;
|
|
begin
|
|
Result := SSE;
|
|
end;
|
|
|
|
{ Degrees of freedom }
|
|
function TFitStatistics.DOF: Integer;
|
|
begin
|
|
Result := N - M;
|
|
end;
|
|
|
|
function TFitStatistics.F: Double;
|
|
begin
|
|
if (M > 1) and (N <> M) and (SSE <> 0) then
|
|
Result := (SSR/(M-1)) / (SSE/(N-M))
|
|
else
|
|
Result := NaN;
|
|
end;
|
|
|
|
function TFitStatistics.Fcrit: Double;
|
|
begin
|
|
if (M = 1) then
|
|
Result := NaN
|
|
else
|
|
Result := InvFDist(FAlpha, M-1, N-M);
|
|
end;
|
|
|
|
function TFitStatistics.GetVarCovar(i, j: Integer): Double;
|
|
begin
|
|
Result := fVarCovar[i, j];
|
|
end;
|
|
|
|
{ Probability that the scatter of the data around the fitted curve is by chance.
|
|
Should be several 0.1, the higher the better.
|
|
According to Numerical Recipes, very small (<< 0.1) values mean
|
|
- wrong model
|
|
- error bars too small
|
|
- errors are not normally distributed
|
|
Values close to 1.0 mean
|
|
- error bars overestimaged. }
|
|
function TFitStatistics.pValue: Double;
|
|
begin
|
|
if DOF > 0 then
|
|
Result := chi2dist(Chi2, DOF)
|
|
else
|
|
Result := NaN;
|
|
end;
|
|
|
|
{ Variance normalized to the degrees of freedem. Should be about 1 for
|
|
a "moderately" good fit. }
|
|
function TFitStatistics.ReducedChi2: Double;
|
|
begin
|
|
if (DOF > 0) then
|
|
Result := SSE / DOF
|
|
else
|
|
Result := NaN;
|
|
end;
|
|
|
|
{ Coefficient of determination: 0 -> "poor" fit, 1 -> "good" fit }
|
|
function TFitStatistics.R2: Double;
|
|
begin
|
|
Result := SSR / SST;
|
|
end;
|
|
|
|
{ Mean residual standard error of fit: The smaller the better }
|
|
function TFitStatistics.ResidualStdError: Double;
|
|
begin
|
|
if DOF > 0 then
|
|
Result := sqrt(SSE / DOF)
|
|
else
|
|
Result := NaN;
|
|
end;
|
|
|
|
procedure TFitStatistics.Report_ANOVA(AText: TStrings; ASeparator: String = ': ';
|
|
ANumFormat: String = '%f'; AExpFormat: String = '%.3e'; NaNStr: String = 'n/a');
|
|
const
|
|
PRECISION = 3;
|
|
begin
|
|
AText.Add(rsFitNumObservations + ASeparator + IntToStr(N));
|
|
AText.Add(rsFitNumFitParams + ASeparator + IntToStr(M));
|
|
AText.Add(rsFitDegreesOfFreedom + ASeparator + IntToStr(DOF));
|
|
AText.Add(rsFitTotalSumOfSquares + ASeparator + FloatToStrEx(SST, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitRegressionSumOfSquares + ASeparator + FloatToStrEx(SSR, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitErrorSumOfSquares + ASeparator + FloatToStrEx(SSE, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitCoefficientOfDetermination + ASeparator + FloatToStrEx(R2, PRECISION, ANumFormat, '', NaNStr));
|
|
AText.Add(rsFitAdjCoefficientOfDetermination + ASeparator + FloatToStrEx(AdjR2, PRECISION, ANumFormat, '', NaNStr));
|
|
AText.Add(rsFitChiSquared + ASeparator + FloatToStrEx(Chi2, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitReducedChiSquared + ASeparator + FloatToStrEx(ReducedChi2, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitResidualStandardError + ASeparator + FloatToStrEx(ResidualStdError, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitVarianceRatio + ASeparator + FloatToStrEx(F, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
{
|
|
AText.Add(Format('Fcrit(%d, %d)', [M-1, DOF]) + ASeparator +
|
|
Format(IfThen(Fcrit < 1E-3, FMT, ANumFormat), [Fcrit]));
|
|
}
|
|
AText.Add(rsFitTValue + ASeparator + FloatToStrEx(FtValue, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
AText.Add(rsFitPValue + ASeparator + FloatToStrEx(pValue, PRECISION, ANumFormat, AExpFormat, NaNStr));
|
|
end;
|
|
|
|
procedure TFitStatistics.Report_VarCovar(AText: TStrings; ANumFormat: String = '%12.6f');
|
|
var
|
|
i, j: Integer;
|
|
s, t: String;
|
|
w: Integer;
|
|
begin
|
|
t := Copy(ANumFormat, 1, pos('.', ANumFormat)-1);
|
|
Delete(t, 1, 1);
|
|
w := StrToInt(t);
|
|
|
|
for i := 0 to M-1 do begin
|
|
s := '';
|
|
for j := 0 to M-1 do
|
|
if IsNaN(VarCovar[i, j]) then
|
|
s := s + Format('%*s', [w, '---'])
|
|
else
|
|
s := s + Format(ANumFormat, [VarCovar[i, j]]);
|
|
AText.Add(s);
|
|
end;
|
|
end;
|
|
|
|
procedure TFitStatistics.SetAlpha(AValue: Double);
|
|
begin
|
|
fAlpha := AValue;
|
|
CalcTValue;
|
|
end;
|
|
|
|
end.
|
|
|