My task is to plot Least Mean Squared Error (LSME) values from each iteration of a machine learning algorithm in a Graph of X and Y axes/coordinates. I decided to print special character (say *) on the console using loops. I do not want to use any libraries for graph plotting but to be simple by printing sequence of special character so that I may be able to print first quadrant of X-Y coordinates onto console.
I recall my initial programming assignments in Java to print different shapes on console like Pyramid, Square, Rectangle, Circle etc. using for and while loops. Also, I am familiar with NDC to view port mapping in graphics programming. But I am unable to implement such nested loops that print my required graph in first quadrant on console as same that we draw on paper.
On console, the origin (0,0) is top left corner of console. But on paper the origin is left bottom if we only plot first quadrant. For overcoming this problem I cracked an idea that I use a 2 D matrix structure and some transpose operation of it and use characters (BLANK SPACE and *) for plotting my graph. I developed following code which has two arrays, one with error values (LMSE) and the other one with the count of spaces.
use strict;
use warnings;
use Data::Dumper;
$|= 1;
my @values = (0.7,0.9,2,0.1,1.2,2.4,0.4,3.5,4.9); # Float error values with 1 decimal place
my @values2;
my $XAxis_LMSE = scalar @values;
my ($minLMSE_Graph, $maxLMSE_Graph) = (sort {$a <=> $b} @values)[0, -1];
for (my $i = 0; $i < scalar @values; $i ) {
my $rem = $maxLMSE_Graph - $values[$i];
push (@values2, $rem);
}
I computed maximum value of my error values array and assigned the difference of Max value with original error value to another array. The logic which I am able to conceive is that I fill a matrix with spaces and * which when printed on console depict a X-Y first quadrant graph on console. Is my approach promising? Can somebody confirm my approach is correct and how to build such a matrix of " " and "*" characters?
Y(x) values are given by array @values and X is number of Iterations. Iterations can go from 1 to say 100. While Y(x) also remains an Integer. Its a simple Column Bar Graph. Below is a sample graph in Excel but the column Bars will be series of character "*" on console. It will be a vertical Bar Graph.
CodePudding user response:
Update in requirements is a game-changer. See discussion and code furhter below.
One way, with originally posted integer data (see below for updated requirements)
use warnings;
use strict;
use feature 'say';
use List::Util qw(max min);
# =================================
# Data posted originally (integer):
# in code; with a negative value added; in Excel graph
# ======================================================
my @vals = (7,9,2,0,1,2,4,3,9);
#my @vals = (7,9,2,0,1,2,4,3,-2,9);
#my @vals = (38, 32, 28, 29, 34, 31, 15, 43, 43, 11, 4, 34);
my $max_y = max @vals;
my $min_y = min @vals;
my $min_y_to_show = ($min_y >= 0) ? 1 : $min_y;
for my $y (reverse $min_y_to_show .. $max_y) {
printf "- | ", $y; # y-axis: value for this row (and "axis")
say join '',
map { $_ >= $y ? ' * ' : ' 'x3 } @vals;
}
# x-axis, with its values
say ' 'x4, '-'x(3*@vals);
say ' 'x4, join '', map { sprintf "=", $_ } 1..@vals;
Prints
9 | * * 8 | * * 7 | * * * 6 | * * * 5 | * * * 4 | * * * * 3 | * * * * * 2 | * * * * * * * 1 | * * * * * * * * --------------------------- 1 2 3 4 5 6 7 8 9
I've made a few presentational choices of substance: to always plot down to 1
(even if all data are greater) and to not show zero -- unless there are negative values, when all is shown (add a negative value to @vals
to test). These are changed fairly easily.
There's also some trivial formatting choices, for layout/spacing etc.
Otherwise there isn't anything manual really. Change @vals
to plot a different data set,† hopefully in the same style. This wasn't tested much.
Update in the question introduces floating point (decimal) values. This is further elaborated in comments, what altogether amounts to a library-grade project. And some of these wants are just not possible in ASCII in a terminal, where "plotting" goes by character and we only have a 100 or so. Here is code updated for what is feasible here, and some discussion.
To accommodate floating point values (with one digit of precision we are told), the y-axis now need be plotted in smaller increments ("divisions" -- "ticks"), lest we fail to show a lot of data if they are lumped within an integer.
Then, how to divide it? Below I show all data within 20 rows, and with a row for the smallest value added if needed. From that a division is worked out, for the given data set (updated in the question). If the data are clustered around some value far from zero then this isn't good of course (imagine data between 2.8 and 3.9, going by 0.1; why would we plot bars all the way from zero?). But one has to make decisions for a given data set, what can be done automatically as well.
This necessarily leads to some imprecision in how data is shown. Showing every data point correctly isn't feasible in general in a terminal.
use warnings;
use strict;
use feature 'say';
use List::Util qw(max min);
my @vals = (0.7, 0.9, 2, 0.1, 1.2, 2.4, 0.4, 3.5, 4.9);
my $n_rows = 20;
my $max_y = max @vals;
my $min_y = min @vals;
# Show from at least the smallest y-division ("tick");
# at first use 0 and then work out the "tick" and adjust
my $min_y_to_show = $min_y >= 0 ? 0 : $min_y;
my $y_tick = ($max_y - $min_y_to_show) / $n_rows;
# Now once we have the y-division ("tick") adjust
$min_y_to_show = $min_y >= $y_tick ? $y_tick : $min_y;
say "Smallest division for y = $y_tick\n";
my @y_axis = map { $y_tick * $_ } 1 .. $n_rows;
unshift @y_axis, $min_y_to_show if $min_y_to_show < $y_axis[0];
for my $y (reverse @y_axis) {
printf "%4.2f | ", $y;
say join '',
map { $_ >= $y ? ' * ' : ' 'x3 } @vals;
}
say ' 'x6, '-'x(3*@vals);
say ' 'x6, join '', map { sprintf "=", $_ } 1..@vals;
Prints
Smallest division for y = 0.245 4.90 | * 4.66 | * 4.41 | * 4.17 | * 3.92 | * 3.68 | * 3.43 | * * 3.19 | * * 2.94 | * * 2.70 | * * 2.45 | * * 2.21 | * * * 1.96 | * * * * 1.72 | * * * * 1.47 | * * * * 1.23 | * * * * 0.98 | * * * * * 0.74 | * * * * * * 0.49 | * * * * * * * 0.25 | * * * * * * * * 0.10 | * * * * * * * * * --------------------------- 1 2 3 4 5 6 7 8 9
In further discussion in comments it is explained that x
-values may actually go into hundreds. That would have to be scaled (can't show 500 data points in a 100-char wide terminal) but then that comes with further decisions to make since not all data can be shown.
This amounts to much too much for a Stackoverflow Q-A. There are just too many details to be specified and decided on. Hopefully the discussion and code above is helpful for people to work out more elaborate scenarios.
Finally, if all this adds up to too much I can recommend gnuplot
used out of Perl. It produces publication quality plots and it's fairly simple to use for simple things -- once learned, what isn't a terrible task with all the resources and example out there.
Otherwise, there are a number of other Perl libraries for graphing of various kinds.
† This is for data shown in the original version of the question (seen in the code here)
With the values picked from the image of an Excel graph shown in the question, instead of the @vals
used above (from the question's code), it prints
43 | * * 42 | * * 41 | * * 40 | * * 39 | * * 38 | * * * 37 | * * * 36 | * * * 35 | * * * 34 | * * * * * 33 | * * * * * 32 | * * * * * * 31 | * * * * * * * 30 | * * * * * * * 29 | * * * * * * * * 28 | * * * * * * * * * 27 | * * * * * * * * * 26 | * * * * * * * * * 25 | * * * * * * * * * 24 | * * * * * * * * * 23 | * * * * * * * * * 22 | * * * * * * * * * 21 | * * * * * * * * * 20 | * * * * * * * * * 19 | * * * * * * * * * 18 | * * * * * * * * * 17 | * * * * * * * * * 16 | * * * * * * * * * 15 | * * * * * * * * * * 14 | * * * * * * * * * * 13 | * * * * * * * * * * 12 | * * * * * * * * * * 11 | * * * * * * * * * * * 10 | * * * * * * * * * * * 9 | * * * * * * * * * * * 8 | * * * * * * * * * * * 7 | * * * * * * * * * * * 6 | * * * * * * * * * * * 5 | * * * * * * * * * * * 4 | * * * * * * * * * * * * 3 | * * * * * * * * * * * * 2 | * * * * * * * * * * * * 1 | * * * * * * * * * * * * ------------------------------------ 1 2 3 4 5 6 7 8 9 10 11 12
CodePudding user response:
While libraries are dismissed by the question, the requirements elaborated in comments (large x-axis span, floating point values, particular data ranges) make it hard to do justice to the problem by a hand-rolled snippet like in my other answer.
So here is an example using the great gnuplot library, which has many features and publication quality plots. Learning it isn't hard, with lots of resources and examples.
The library does have an option to print to what it calls "dumb" terminal, ie. ascii, the main requirement here. There are many other options, not exercised in this basic example.
There is also a wrapper module for Perl, Chart::Gnuplot, which makes it simpler yet.†
use warnings;
use strict;
use feature 'say';
use Path::Tiny; # path()->slurp
use List::Util qw(max min);
use Chart::Gnuplot;
#my $outfile = shift // 'ascii_gnuplot.out';
my (@x, @y);
@y = (0.7, 0.9, 2, 0.1, 1.2, 2.4, 0.4, 3.5, 4.9);
@x = 1 .. @y;
# Or, to see how it goes about large span of x-axis
#demo_large_x_span(\@x, \@y);
my $y_min = min @y;
my $yrange_min = $y_min > 0 ? 0 : $y_min;
my $chart = Chart::Gnuplot->new(
terminal => 'dumb', # ascii
xrange => [min(@x), max @x],
yrange => [$yrange_min, max @y],
#output => $outfile,
title => "Least Mean Squared Error with iterations",
);
my $dataset = Chart::Gnuplot::DataSet->new(
style => "impulses", # "dots" for normal scatter plot
xdata => \@x,
ydata => \@y,
);
$chart->plot2d($dataset); # done. shows on screen or in "output" file
sub demo_large_x_span {
my ($xref, $yref, $max_x) = @_;
$max_x //= 200;
for (1 .. $max_x) {
if ($_ % 10 == 0) {
push @$xref, $_;
push @$yref, sqrt($_); #$_*0.5;
}
}
}
If this fails to print the ascii graph to terminal then uncomment the output => $outfile
option (and the filename's definition). Then open and print the file out of the program.‡
The above prints
Least Mean Squared Error with iterations -------- ------- -------- -------- ------- -------- ------- --------* * 4.5 * 4 * | * 3.5 * * | * * 3 * * | * * 2.5 * * | * * * 2 * * * * | * * * * 1.5 * * * * | * * * * * 1 * * * * * * * * * * * * 0.5 * * * * * * * * * * * * * * * * 0 * -------*-------*--------*--------*-------*--------*-------*------- * 1 2 3 4 5 6 7 8 9
I suggest to review the many options available -- remove top y-axis? remove extra
's? add labels anywhere, to data or plot? timestamp? format axes? plot a histogram instead? etc.
A nice overview is in gnuplot demos and then in Chart::Gnuplot gallery and examples.
Finally, a grand advantage of using a library for this is that by "flipping a switch" one can have a top-notch plot in one of many formats (change terminal
setting, or simply drop it and name the output file with the extension of the desired graphics format).
† Use of this module isn't necessary. One can write a file with specifications for gnuplot
and then run gnuplot
on that file, out of the program. (I'd consider this to be an overall simpler way, if one is comfortable with gnuplot
's commands.)
‡ The file to which gnuplot
prints can be made using File::Temp so that it is removed on its own, if you don't want to keep it.