YOUSEFUL / DELPHI / SAMPLE PROJECTS

Statistics console

Console program Self-checking Product: Atlas Plot Pro · Components: TAtlasStats, TAtlasHeatMap, TAtlasBoxPlot

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:

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

Example
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

Project files

Download all 34 projects

The ZIP contains every sample project with its README. See getting started for the search path to add.

Source

StatisticsConsole.dpr

pascal
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.