YOUSEFUL / DELPHI / SAMPLE PROJECTS
Statistics console
The quick start of Atlas Plot Pro, and the project of its
tutorial. A console program
that runs every function of TAtlasStats on small data sets whose answers can
be worked out by hand, then draws a heat map and a box plot into files without
creating a window:
- quantiles, mean and sample standard deviation of 1..10 given in shuffled order;
- box statistics of 1..9 plus 40: quartiles, whiskers at 1.5 IQR, one outlier;
- a four-bin histogram with half-open bins and a closed last bin,
BinOf, and Sturges' rule when the bin count is 0; - Pearson correlation, an exact quadratic recovered by
PolynomialFit(also outside the data) and the low R squared of a straight line through the same points; - exponential smoothing with alpha 0.5;
- a fit that
EAtlasPlotErrorrejects because two points cannot fix a parabola; TAtlasHeatMapwith an empty (NaN) cell and a fixed 50..100 scale saved asheatmap.png, andTAtlasBoxPlotwith two services saved asboxplot.svg.
Every result is compared with the expected value; a mismatch raises and the exit code is 1.
Build and run
Open StatisticsConsole.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Plot Pro src folder and the Atlas Plot src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The program needs no input. It checks its own results and stops with an error if any of them differs from the output below.
Expected output
Quantiles of 1..10 in shuffled order
Q1 3.25 median 5.50 Q3 7.75 mean 5.50 sd 3.0277
ok: first quartile interpolates between ranks 3 and 4
ok: median of an even count is the middle pair average
ok: mean and sample standard deviation
Box statistics of 1..9 and one slow value, 40
box 3.25..7.75 whiskers 1..9 1 outlier(s)
ok: quartiles
ok: whiskers stop at the last value within 1.5 IQR
ok: 40 is the only outlier
Histogram of 1 2 2 3 3 3 4 4 5 in four bins
[1, 2): 1
[2, 3): 2
[3, 4): 3
[4, 5]: 3
ok: bins are half open, the last one closed
ok: BinOf finds the bin; outside the range is -1
ok: Bins = 0 applies Sturges' rule: ceil(log2 9) + 1 = 5
Correlation and least-squares fits
r = 0.80
ok: Pearson r of a partly ordered pair is 0.8
quadratic: degree 2, R squared 1.0000, value at 10 = 171.0000
ok: the parabola is recovered, also outside the data
straight line: R squared 0.3047
ok: a line explains less than a third of a parabola
Exponential smoothing of 10 20 20 0 with alpha 0.5
10.00 15.00 17.50 8.75
ok: each value moves halfway towards the input
Invalid input raises EAtlasPlotError
ok: a fit with too few distinct arguments is rejected
Charts rendered without a window
ok: heatmap.png written
ok: NaN is an empty cell; 75 is halfway on the 50..100 scale
ok: boxplot.svg names both categories
ok: the chart keeps the statistics of each sample
All results verified.Next to the executable you find heatmap.png (480 × 300: three node rows, four
hour columns, white-to-black cells with their percentages in contrasting text,
a light grey empty cell for node-b at 06 and a 50..100 colour scale on the
right) and boxplot.svg (two boxes; the 60 ms Gateway request is drawn as an
outlier circle).
What to change first
- Put your own measurements into the arrays and replace the
Expectcalls withWriteln; every function raisesEAtlasPlotErroron NaN, infinite or empty input, so wrap them intry ... except on EAtlasPlotErrorwhen the data comes from outside. - Change the degree passed to
PolynomialFit(1 to 6) and compareRSquared. - The heat map stores percentages (91, not 0.91) because
ValueFormatis aFormatFloatmask, andFormatFloatprints a%sign as it is without multiplying by 100. - Change the extensions passed to
SaveToFile:.png,.bmp,.emfand.svgare supported.
Project files
The ZIP contains every sample project with its README. See getting started for the search path to add.
Source
StatisticsConsole.dpr
program StatisticsConsole;
// Atlas Plot Pro quick start: statistics on known data, then a heat map
// and a box plot written to image files without a window. Every result is
// checked against a value worked out by hand; a mismatch raises and the
// program exits with code 1.
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.StrUtils, System.Math, System.IOUtils, Winapi.Windows, Vcl.Graphics,
Atlas.PlotAxis, Atlas.PlotStats, Atlas.BoxPlot, Atlas.HeatMap;
procedure Expect(Condition: Boolean; const What: string);
begin
if not Condition then raise Exception.Create('Unexpected result: ' + What);
Writeln(' ok: ', What);
end;
function Near(const A, B: Double): Boolean;
begin
Result := Abs(A - B) < 1E-9;
end;
procedure Quantiles;
var Values: TArray<Double>;
begin
Writeln('Quantiles of 1..10 in shuffled order');
Values := [7, 2, 9, 4, 5, 3, 8, 6, 1, 10];
Writeln(Format(' Q1 %.2f median %.2f Q3 %.2f mean %.2f sd %.4f',
[TAtlasStats.Quantile(Values, 0.25), TAtlasStats.Quantile(Values, 0.5),
TAtlasStats.Quantile(Values, 0.75), TAtlasStats.Mean(Values), TAtlasStats.StdDev(Values)]));
Expect(Near(TAtlasStats.Quantile(Values, 0.25), 3.25), 'first quartile interpolates between ranks 3 and 4');
Expect(Near(TAtlasStats.Quantile(Values, 0.5), 5.5), 'median of an even count is the middle pair average');
Expect(Near(TAtlasStats.Mean(Values), 5.5) and Near(TAtlasStats.StdDev(Values), Sqrt(82.5 / 9)), 'mean and sample standard deviation');
end;
procedure BoxStatistics;
var Box: TAtlasBoxStats;
begin
Writeln('Box statistics of 1..9 and one slow value, 40');
Box := TAtlasStats.Box([1, 2, 3, 4, 5, 6, 7, 8, 9, 40]);
Writeln(Format(' box %.2f..%.2f whiskers %.0f..%.0f %d outlier(s)',
[Box.Q1, Box.Q3, Box.LowWhisker, Box.HighWhisker, Length(Box.Outliers)]));
Expect(Near(Box.Q1, 3.25) and Near(Box.Median, 5.5) and Near(Box.Q3, 7.75), 'quartiles');
Expect(Near(Box.LowWhisker, 1) and Near(Box.HighWhisker, 9), 'whiskers stop at the last value within 1.5 IQR');
Expect((Length(Box.Outliers) = 1) and Near(Box.Outliers[0], 40), '40 is the only outlier');
end;
procedure Histogram;
var Bars: TAtlasHistogram; I: Integer;
begin
Writeln('Histogram of 1 2 2 3 3 3 4 4 5 in four bins');
Bars := TAtlasStats.Histogram([1, 2, 2, 3, 3, 3, 4, 4, 5], 4);
for I := 0 to High(Bars.Counts) do
Writeln(Format(' [%.0f, %.0f%s: %d', [Bars.Edges[I], Bars.Edges[I + 1], IfThen(I = High(Bars.Counts), ']', ')'), Bars.Counts[I]]));
Expect((Bars.Counts[0] = 1) and (Bars.Counts[1] = 2) and (Bars.Counts[2] = 3) and (Bars.Counts[3] = 3), 'bins are half open, the last one closed');
Expect((Bars.BinOf(2) = 1) and (Bars.BinOf(5) = 3) and (Bars.BinOf(6) = -1), 'BinOf finds the bin; outside the range is -1');
Expect(Length(TAtlasStats.Histogram([1, 2, 2, 3, 3, 3, 4, 4, 5]).Counts) = 5, 'Bins = 0 applies Sturges'' rule: ceil(log2 9) + 1 = 5');
end;
procedure CorrelationAndFit;
var X, Y: TArray<Double>; I: Integer; Fit: TAtlasPolynomial;
begin
Writeln('Correlation and least-squares fits');
Writeln(Format(' r = %.2f', [TAtlasStats.Correlation([1, 2, 3, 4, 5], [2, 1, 4, 3, 5])]));
Expect(Near(TAtlasStats.Correlation([1, 2, 3, 4, 5], [2, 1, 4, 3, 5]), 0.8), 'Pearson r of a partly ordered pair is 0.8');
SetLength(X, 9);
SetLength(Y, 9);
for I := 0 to 8 do
begin
X[I] := I - 4;
Y[I] := 2 * X[I] * X[I] - 3 * X[I] + 1; // an exact parabola
end;
Fit := TAtlasStats.PolynomialFit(X, Y, 2);
Writeln(Format(' quadratic: degree %d, R squared %.4f, value at 10 = %.4f', [Fit.Degree, Fit.RSquared, Fit.Evaluate(10)]));
Expect((Fit.Degree = 2) and Near(Fit.RSquared, 1) and (Abs(Fit.Evaluate(10) - 171) < 1E-6), 'the parabola is recovered, also outside the data');
Fit := TAtlasStats.PolynomialFit(X, Y, 1);
Writeln(Format(' straight line: R squared %.4f', [Fit.RSquared]));
Expect(Abs(Fit.RSquared - 0.30474) < 1E-4, 'a line explains less than a third of a parabola');
end;
procedure Smoothing;
var Smooth: TArray<Double>;
begin
Writeln('Exponential smoothing of 10 20 20 0 with alpha 0.5');
Smooth := TAtlasStats.ExponentialSmoothing([10, 20, 20, 0], 0.5);
Writeln(Format(' %.2f %.2f %.2f %.2f', [Smooth[0], Smooth[1], Smooth[2], Smooth[3]]));
Expect(Near(Smooth[0], 10) and Near(Smooth[1], 15) and Near(Smooth[2], 17.5) and Near(Smooth[3], 8.75), 'each value moves halfway towards the input');
end;
procedure Rejections;
var Rejected: Boolean;
begin
Writeln('Invalid input raises EAtlasPlotError');
Rejected := False;
try
TAtlasStats.PolynomialFit([1, 2], [3, 4], 2); // two points cannot fix a parabola
except
on EAtlasPlotError do Rejected := True;
end;
Expect(Rejected, 'a fit with too few distinct arguments is rejected');
end;
procedure ExportCharts;
var Map: TAtlasHeatMap; Box: TAtlasBoxPlot; Svg: string;
begin
Writeln('Charts rendered without a window');
Map := TAtlasHeatMap.Create(nil);
try
Map.Title := 'Cache hit rate (%) by node and hour';
Map.RowLabels.CommaText := 'node-a,node-b,node-c';
Map.ColumnLabels.CommaText := '00,06,12,18';
// Percentages, because FormatFloat prints a % sign as it is and does not multiply by 100.
Map.LoadMatrix([[91, 88, 72, 80], [93, NaN, 75, 83], [89, 86, 70, 79]]);
Map.LowColor := clWhite;
Map.HighColor := clBlack;
Map.SetScale(50, 100);
Map.ShowValues := True;
Map.ValueFormat := '0"%"';
Map.SaveToFile('heatmap.png', 480, 300);
Expect(TFile.GetSize('heatmap.png') > 1000, 'heatmap.png written');
Expect(Map.IsEmpty(1, 1) and (Map.ColorOf(75) = TColor(RGB(128, 128, 128))), 'NaN is an empty cell; 75 is halfway on the 50..100 scale');
finally
Map.Free;
end;
Box := TAtlasBoxPlot.Create(nil);
try
Box.Title := 'Response time by service';
Box.ValueAxis.Title := 'ms';
Box.AddCategory('Gateway', [12, 15, 15, 17, 18, 18, 18, 20, 21, 60]);
Box.AddCategory('Search', [30, 31, 35, 36, 38, 40, 41, 45]);
Box.SaveToFile('boxplot.svg', 480, 300);
Svg := TFile.ReadAllText('boxplot.svg', TEncoding.UTF8);
Expect((Pos('>Gateway<', Svg) > 0) and (Pos('>Search<', Svg) > 0), 'boxplot.svg names both categories');
Expect(Near(Box.Stats(0).Median, 18) and (Length(Box.Stats(0).Outliers) = 1), 'the chart keeps the statistics of each sample');
finally
Box.Free;
end;
end;
begin
try
FormatSettings := TFormatSettings.Invariant;
Quantiles;
BoxStatistics;
Histogram;
CorrelationAndFit;
Smoothing;
Rejections;
ExportCharts;
Writeln('All results verified.');
except
on E: Exception do
begin
Writeln(E.ClassName, ': ', E.Message);
ExitCode := 1;
end;
end;
end.