3
\$\begingroup\$

I have written a small program in C# to plot ROC, PR (equal to AP) and PRI (equal to API) curves using the plot_curve() method. (The program also calculates the approximate AUC of the curves using the area_under_curve_trapz() method, but that isn't the main point. The main point and purpose is to plot the curves.)

Plotting ROC and PR curves in C# appears to be a task which has not yet been done effectively. Therefore, this algorithm is written from scratch. I also hope this algorithm will be useful for those interested in data science and machine learning, especially those using C#.

Please review and suggest any improvements in the algorithm. (In particular, I am curious about better methods of averaging the x,y coordinates for the outer-cross-validation results (i.e. nested-cross-validation), considering the variable decision boundary thresholds, i.e. the input of x,y can never be pre-determined, and using the standard 11 point averaging does not give accurate results; rather a very poor approximation. Still, it is supported by the method.)

The whole (working) algorithm is in the following file - plot.cs. It can be tested using the static plot.test_plot() method which contains some real sample data. The generated bmp images will be loaded in the default program after execution.

Note that this was written using Visual Studio 2017, C# 7.3 and .NET 4.7.2. It may be incompatible with some lower C# or .NET versions. Please also note that this algorithm purposely ignores standard C# naming conventions.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using System.Linq;

namespace svm_compute
{
    public static class plot
    {
        public static void test_plot()
        {
            // this function plots sample ROC, PR and PRI (approximated interpolated) curves, using all thresholds and 11 point thresholds.
            var encoded_roc = @"
FPR;TPR/0;0/0;0.022222/0;0.044444/0.030303;0.044444/0.030303;0.066667/0.030303;0.088889/0.030303;0.111111/0.030303;0.133333/0.030303;0.155556/0.030303;0.177778/0.060606;0.177778/0.060606;0.2/0.060606;0.222222/0.060606;0.244444/0.060606;0.266667/0.060606;0.288889/0.090909;0.288889/0.090909;0.311111/0.090909;0.333333/0.121212;0.333333/0.121212;0.355556/0.151515;0.355556/0.151515;0.377778/0.181818;0.377778/0.181818;0.4/0.181818;0.422222/0.181818;0.444444/0.181818;0.466667/0.212121;0.466667/0.212121;0.488889/0.242424;0.488889/0.242424;0.511111/0.242424;0.533333/0.242424;0.555556/0.242424;0.577778/0.242424;0.6/0.242424;0.622222/0.242424;0.644444/0.242424;0.666667/0.272727;0.666667/0.272727;0.688889/0.30303;0.688889/0.333333;0.688889/0.333333;0.711111/0.333333;0.733333/0.333333;0.755556/0.363636;0.755556/0.363636;0.777778/0.363636;0.8/0.363636;0.822222/0.393939;0.822222/0.393939;0.844444/0.393939;0.866667/0.393939;0.888889/0.424242;0.888889/0.424242;0.911111/0.424242;0.933333/0.454545;0.933333/0.484848;0.933333/0.515152;0.933333/0.515152;0.955556/0.515152;0.977778/0.545455;0.977778/0.545455;1/0.606061;1/0.636364;1/0.666667;1/0.69697;1/0.727273;1/0.757576;1/0.787879;1/0.818182;1/0.848485;1/0.878788;1/0.909091;1/0.939394;1/0.969697;1/1;1
FPR;TPR/0;0/0;0.030303/0;0.060606/0;0.090909/0;0.121212/0;0.151515/0;0.181818/0;0.212121/0;0.242424/0;0.272727/0;0.30303/0;0.333333/0;0.363636/0;0.393939/0;0.454545/0.022222;0.454545/0.022222;0.484848/0.044444;0.484848/0.066667;0.484848/0.066667;0.515152/0.066667;0.545455/0.066667;0.575758/0.088889;0.575758/0.111111;0.575758/0.111111;0.606061/0.133333;0.606061/0.155556;0.606061/0.177778;0.606061/0.177778;0.636364/0.2;0.636364/0.222222;0.636364/0.244444;0.636364/0.244444;0.666667/0.266667;0.666667/0.288889;0.666667/0.311111;0.666667/0.311111;0.69697/0.311111;0.727273/0.333333;0.727273/0.333333;0.757576/0.355556;0.757576/0.377778;0.757576/0.4;0.757576/0.422222;0.757576/0.444444;0.757576/0.466667;0.757576/0.488889;0.757576/0.511111;0.757576/0.511111;0.787879/0.533333;0.787879/0.533333;0.818182/0.555556;0.818182/0.577778;0.818182/0.6;0.818182/0.622222;0.818182/0.622222;0.848485/0.644444;0.848485/0.644444;0.878788/0.666667;0.878788/0.666667;0.909091/0.688889;0.909091/0.711111;0.909091/0.711111;0.939394/0.733333;0.939394/0.755556;0.939394/0.777778;0.939394/0.8;0.939394/0.822222;0.939394/0.822222;0.969697/0.844444;0.969697/0.866667;0.969697/0.888889;0.969697/0.911111;0.969697/0.933333;0.969697/0.955556;0.969697/0.955556;1/0.977778;1/1;1
FPR;TPR/0;0/0;0.022222/0;0.044444/0.030303;0.044444/0.030303;0.066667/0.030303;0.088889/0.030303;0.111111/0.030303;0.133333/0.030303;0.155556/0.030303;0.177778/0.030303;0.2/0.060606;0.2/0.060606;0.222222/0.060606;0.244444/0.060606;0.266667/0.060606;0.288889/0.060606;0.311111/0.090909;0.311111/0.121212;0.311111/0.121212;0.333333/0.121212;0.355556/0.121212;0.377778/0.121212;0.4/0.121212;0.422222/0.121212;0.444444/0.151515;0.444444/0.151515;0.466667/0.151515;0.488889/0.181818;0.488889/0.181818;0.511111/0.212121;0.511111/0.242424;0.511111/0.242424;0.533333/0.242424;0.555556/0.242424;0.577778/0.242424;0.6/0.272727;0.6/0.272727;0.622222/0.272727;0.644444/0.272727;0.666667/0.272727;0.688889/0.30303;0.688889/0.30303;0.711111/0.333333;0.711111/0.333333;0.733333/0.363636;0.733333/0.393939;0.733333/0.424242;0.733333/0.424242;0.755556/0.424242;0.777778/0.424242;0.8/0.424242;0.822222/0.424242;0.844444/0.424242;0.866667/0.424242;0.888889/0.454545;0.888889/0.484848;0.888889/0.484848;0.911111/0.515152;0.911111/0.545455;0.911111/0.545455;0.933333/0.575758;0.933333/0.606061;0.933333/0.606061;0.955556/0.666667;0.955556/0.69697;0.955556/0.69697;0.977778/0.727273;0.977778/0.757576;0.977778/0.787879;0.977778/0.787879;1/0.818182;1/0.848485;1/0.878788;1/0.909091;1/0.939394;1/0.969697;1/1;1
FPR;TPR/0;0/0;0.030303/0;0.060606/0;0.090909/0;0.121212/0;0.151515/0;0.181818/0;0.212121/0.022222;0.212121/0.022222;0.242424/0.022222;0.272727/0.022222;0.30303/0.044444;0.30303/0.044444;0.333333/0.044444;0.393939/0.066667;0.393939/0.066667;0.424242/0.066667;0.454545/0.088889;0.454545/0.088889;0.484848/0.088889;0.515152/0.111111;0.515152/0.111111;0.545455/0.111111;0.575758/0.133333;0.575758/0.155556;0.575758/0.177778;0.575758/0.2;0.575758/0.222222;0.575758/0.244444;0.575758/0.266667;0.575758/0.266667;0.606061/0.266667;0.636364/0.266667;0.666667/0.288889;0.666667/0.288889;0.69697/0.311111;0.69697/0.311111;0.727273/0.333333;0.727273/0.355556;0.727273/0.377778;0.727273/0.4;0.727273/0.4;0.757576/0.422222;0.757576/0.444444;0.757576/0.466667;0.757576/0.488889;0.757576/0.488889;0.787879/0.488889;0.818182/0.511111;0.818182/0.511111;0.848485/0.533333;0.848485/0.555556;0.848485/0.555556;0.878788/0.577778;0.878788/0.6;0.878788/0.622222;0.878788/0.644444;0.878788/0.666667;0.878788/0.688889;0.878788/0.688889;0.909091/0.688889;0.939394/0.711111;0.939394/0.733333;0.939394/0.755556;0.939394/0.777778;0.939394/0.8;0.939394/0.8;0.969697/0.822222;0.969697/0.844444;0.969697/0.866667;0.969697/0.888889;0.969697/0.911111;0.969697/0.933333;0.969697/0.955556;0.969697/0.955556;1/0.977778;1/1;1
FPR;TPR/0;0/0;0.022222/0;0.044444/0;0.066667/0;0.088889/0;0.111111/0;0.133333/0;0.155556/0;0.177778/0.030303;0.177778/0.030303;0.2/0.060606;0.2/0.060606;0.222222/0.060606;0.244444/0.060606;0.266667/0.090909;0.266667/0.090909;0.288889/0.090909;0.311111/0.090909;0.333333/0.121212;0.333333/0.121212;0.355556/0.121212;0.377778/0.121212;0.4/0.121212;0.422222/0.151515;0.422222/0.151515;0.444444/0.151515;0.466667/0.151515;0.488889/0.151515;0.511111/0.151515;0.533333/0.151515;0.555556/0.151515;0.577778/0.181818;0.577778/0.181818;0.6/0.181818;0.622222/0.212121;0.622222/0.212121;0.644444/0.242424;0.644444/0.272727;0.644444/0.272727;0.666667/0.272727;0.688889/0.30303;0.688889/0.333333;0.688889/0.333333;0.711111/0.333333;0.733333/0.333333;0.755556/0.333333;0.777778/0.363636;0.777778/0.393939;0.777778/0.424242;0.777778/0.424242;0.8/0.454545;0.8/0.454545;0.822222/0.454545;0.844444/0.454545;0.866667/0.454545;0.888889/0.484848;0.888889/0.484848;0.911111/0.515152;0.911111/0.545455;0.911111/0.545455;0.933333/0.575758;0.933333/0.575758;0.955556/0.606061;0.955556/0.636364;0.955556/0.666667;0.955556/0.666667;0.977778/0.69697;0.977778/0.727273;0.977778/0.757576;0.977778/0.787879;0.977778/0.818182;0.977778/0.848485;0.977778/0.878788;0.977778/0.909091;0.977778/0.909091;1/0.939394;1/0.969697;1/1;1
FPR;TPR/0;0/0;0.030303/0;0.060606/0;0.090909/0.022222;0.090909/0.022222;0.121212/0.022222;0.151515/0.022222;0.181818/0.022222;0.212121/0.022222;0.242424/0.022222;0.272727/0.022222;0.30303/0.022222;0.333333/0.044444;0.333333/0.044444;0.363636/0.044444;0.393939/0.044444;0.424242/0.066667;0.424242/0.066667;0.454545/0.088889;0.454545/0.088889;0.484848/0.088889;0.515152/0.111111;0.515152/0.111111;0.545455/0.133333;0.545455/0.155556;0.545455/0.177778;0.545455/0.2;0.545455/0.2;0.575758/0.222222;0.575758/0.222222;0.606061/0.222222;0.636364/0.222222;0.666667/0.244444;0.666667/0.266667;0.666667/0.288889;0.666667/0.311111;0.666667/0.311111;0.69697/0.311111;0.727273/0.333333;0.727273/0.355556;0.727273/0.355556;0.757576/0.355556;0.787879/0.377778;0.787879/0.377778;0.818182/0.4;0.818182/0.422222;0.818182/0.422222;0.848485/0.444444;0.848485/0.466667;0.848485/0.488889;0.848485/0.511111;0.848485/0.533333;0.848485/0.555556;0.848485/0.577778;0.848485/0.577778;0.878788/0.6;0.878788/0.622222;0.878788/0.644444;0.878788/0.666667;0.878788/0.666667;0.909091/0.688889;0.909091/0.711111;0.909091/0.733333;0.909091/0.733333;0.939394/0.755556;0.939394/0.777778;0.939394/0.8;0.939394/0.8;0.969697/0.822222;0.969697/0.822222;1/0.844444;1/0.866667;1/0.888889;1/0.911111;1/0.933333;1/0.955556;1/0.977778;1/1;1
FPR;TPR/0;0/0;0.022222/0;0.044444/0;0.066667/0;0.088889/0;0.111111/0;0.133333/0;0.155556/0;0.177778/0;0.2/0;0.222222/0;0.244444/0;0.266667/0;0.288889/0;0.311111/0;0.333333/0.030303;0.333333/0.030303;0.355556/0.030303;0.377778/0.030303;0.4/0.030303;0.422222/0.060606;0.422222/0.060606;0.444444/0.060606;0.466667/0.060606;0.488889/0.090909;0.488889/0.090909;0.511111/0.090909;0.533333/0.090909;0.555556/0.121212;0.555556/0.121212;0.577778/0.151515;0.577778/0.151515;0.6/0.151515;0.622222/0.181818;0.622222/0.181818;0.644444/0.181818;0.666667/0.212121;0.666667/0.212121;0.688889/0.242424;0.688889/0.242424;0.711111/0.272727;0.711111/0.30303;0.711111/0.333333;0.711111/0.363636;0.711111/0.363636;0.733333/0.363636;0.755556/0.393939;0.755556/0.393939;0.777778/0.424242;0.777778/0.424242;0.8/0.454545;0.8/0.454545;0.822222/0.484848;0.822222/0.515152;0.822222/0.545455;0.822222/0.545455;0.844444/0.545455;0.866667/0.545455;0.888889/0.545455;0.911111/0.575758;0.911111/0.606061;0.911111/0.606061;0.933333/0.636364;0.933333/0.666667;0.933333/0.666667;0.955556/0.69697;0.955556/0.727273;0.955556/0.727273;0.977778/0.757576;0.977778/0.757576;1/0.787879;1/0.818182;1/0.848485;1/0.878788;1/0.909091;1/0.939394;1/0.969697;1/1;1
FPR;TPR/0;0/0;0.030303/0;0.060606/0;0.090909/0;0.121212/0;0.151515/0;0.181818/0;0.212121/0;0.242424/0.022222;0.242424/0.022222;0.272727/0.044444;0.272727/0.044444;0.30303/0.044444;0.333333/0.066667;0.333333/0.066667;0.363636/0.066667;0.393939/0.088889;0.393939/0.088889;0.424242/0.088889;0.454545/0.111111;0.454545/0.133333;0.454545/0.155556;0.454545/0.177778;0.454545/0.177778;0.484848/0.177778;0.515152/0.177778;0.545455/0.2;0.545455/0.2;0.575758/0.222222;0.575758/0.222222;0.606061/0.244444;0.606061/0.244444;0.636364/0.266667;0.636364/0.288889;0.636364/0.288889;0.666667/0.288889;0.69697/0.288889;0.727273/0.288889;0.757576/0.311111;0.757576/0.311111;0.787879/0.333333;0.787879/0.333333;0.818182/0.355556;0.818182/0.377778;0.818182/0.377778;0.848485/0.4;0.848485/0.422222;0.848485/0.422222;0.878788/0.444444;0.878788/0.444444;0.909091/0.466667;0.909091/0.488889;0.909091/0.511111;0.909091/0.511111;0.939394/0.533333;0.939394/0.555556;0.939394/0.577778;0.939394/0.577778;0.969697/0.6;0.969697/0.622222;0.969697/0.644444;0.969697/0.666667;0.969697/0.666667;1/0.688889;1/0.711111;1/0.733333;1/0.755556;1/0.777778;1/0.8;1/0.822222;1/0.844444;1/0.866667;1/0.888889;1/0.911111;1/0.933333;1/0.955556;1/0.977778;1/1;1
";

            var encoded_pr = @"
TPR;PPV/0;1/0.022222;1/0.044444;1/0.044444;0.666667/0.066667;0.75/0.088889;0.8/0.111111;0.833333/0.133333;0.857143/0.155556;0.875/0.177778;0.888889/0.177778;0.8/0.2;0.818182/0.222222;0.833333/0.244444;0.846154/0.266667;0.857143/0.288889;0.866667/0.288889;0.8125/0.311111;0.823529/0.333333;0.833333/0.333333;0.789474/0.355556;0.8/0.355556;0.761905/0.377778;0.772727/0.377778;0.73913/0.4;0.75/0.422222;0.76/0.444444;0.769231/0.466667;0.777778/0.466667;0.75/0.488889;0.758621/0.488889;0.733333/0.511111;0.741935/0.533333;0.75/0.555556;0.757576/0.577778;0.764706/0.6;0.771429/0.622222;0.777778/0.644444;0.783784/0.666667;0.789474/0.666667;0.769231/0.688889;0.775/0.688889;0.756098/0.688889;0.738095/0.711111;0.744186/0.733333;0.75/0.755556;0.755556/0.755556;0.73913/0.777778;0.744681/0.8;0.75/0.822222;0.755102/0.822222;0.74/0.844444;0.745098/0.866667;0.75/0.888889;0.754717/0.888889;0.740741/0.911111;0.745455/0.933333;0.75/0.933333;0.736842/0.933333;0.724138/0.933333;0.711864/0.955556;0.716667/0.977778;0.721311/0.977778;0.709677/1;0.714286/1;0.692308/1;0.681818/1;0.671642/1;0.661765/1;0.652174/1;0.642857/1;0.633803/1;0.625/1;0.616438/1;0.608108/1;0.6/1;0.592105/1;0.584416/1;0.576923
TPR;PPV/0;1/0.030303;1/0.060606;1/0.090909;1/0.121212;1/0.151515;1/0.181818;1/0.212121;1/0.242424;1/0.272727;1/0.30303;1/0.333333;1/0.363636;1/0.393939;1/0.454545;1/0.454545;0.9375/0.484848;0.941176/0.484848;0.888889/0.484848;0.842105/0.515152;0.85/0.545455;0.857143/0.575758;0.863636/0.575758;0.826087/0.575758;0.791667/0.606061;0.8/0.606061;0.769231/0.606061;0.740741/0.606061;0.714286/0.636364;0.724138/0.636364;0.7/0.636364;0.677419/0.636364;0.65625/0.666667;0.666667/0.666667;0.647059/0.666667;0.628571/0.666667;0.611111/0.69697;0.621622/0.727273;0.631579/0.727273;0.615385/0.757576;0.625/0.757576;0.609756/0.757576;0.595238/0.757576;0.581395/0.757576;0.568182/0.757576;0.555556/0.757576;0.543478/0.757576;0.531915/0.757576;0.520833/0.787879;0.530612/0.787879;0.52/0.818182;0.529412/0.818182;0.519231/0.818182;0.509434/0.818182;0.5/0.818182;0.490909/0.848485;0.5/0.848485;0.491228/0.878788;0.5/0.878788;0.491525/0.909091;0.5/0.909091;0.491803/0.909091;0.483871/0.939394;0.492063/0.939394;0.484375/0.939394;0.476923/0.939394;0.469697/0.939394;0.462687/0.939394;0.455882/0.969697;0.463768/0.969697;0.457143/0.969697;0.450704/0.969697;0.444444/0.969697;0.438356/0.969697;0.432432/0.969697;0.426667/1;0.434211/1;0.428571/1;0.423077
TPR;PPV/0;1/0.022222;1/0.044444;1/0.044444;0.666667/0.066667;0.75/0.088889;0.8/0.111111;0.833333/0.133333;0.857143/0.155556;0.875/0.177778;0.888889/0.2;0.9/0.2;0.818182/0.222222;0.833333/0.244444;0.846154/0.266667;0.857143/0.288889;0.866667/0.311111;0.875/0.311111;0.823529/0.311111;0.777778/0.333333;0.789474/0.355556;0.8/0.377778;0.809524/0.4;0.818182/0.422222;0.826087/0.444444;0.833333/0.444444;0.8/0.466667;0.807692/0.488889;0.814815/0.488889;0.785714/0.511111;0.793103/0.511111;0.766667/0.511111;0.741935/0.533333;0.75/0.555556;0.757576/0.577778;0.764706/0.6;0.771429/0.6;0.75/0.622222;0.756757/0.644444;0.763158/0.666667;0.769231/0.688889;0.775/0.688889;0.756098/0.711111;0.761905/0.711111;0.744186/0.733333;0.75/0.733333;0.733333/0.733333;0.717391/0.733333;0.702128/0.755556;0.708333/0.777778;0.714286/0.8;0.72/0.822222;0.72549/0.844444;0.730769/0.866667;0.735849/0.888889;0.740741/0.888889;0.727273/0.888889;0.714286/0.911111;0.719298/0.911111;0.706897/0.911111;0.694915/0.933333;0.7/0.933333;0.688525/0.933333;0.677419/0.955556;0.68254/0.955556;0.661538/0.955556;0.651515/0.977778;0.656716/0.977778;0.647059/0.977778;0.637681/0.977778;0.628571/1;0.633803/1;0.625/1;0.616438/1;0.608108/1;0.6/1;0.592105/1;0.584416/1;0.576923
TPR;PPV/0;1/0.030303;1/0.060606;1/0.090909;1/0.121212;1/0.151515;1/0.181818;1/0.212121;1/0.212121;0.875/0.242424;0.888889/0.272727;0.9/0.30303;0.909091/0.30303;0.833333/0.333333;0.846154/0.393939;0.866667/0.393939;0.8125/0.424242;0.823529/0.454545;0.833333/0.454545;0.789474/0.484848;0.8/0.515152;0.809524/0.515152;0.772727/0.545455;0.782609/0.575758;0.791667/0.575758;0.76/0.575758;0.730769/0.575758;0.703704/0.575758;0.678571/0.575758;0.655172/0.575758;0.633333/0.575758;0.612903/0.606061;0.625/0.636364;0.636364/0.666667;0.647059/0.666667;0.628571/0.69697;0.638889/0.69697;0.621622/0.727273;0.631579/0.727273;0.615385/0.727273;0.6/0.727273;0.585366/0.727273;0.571429/0.757576;0.581395/0.757576;0.568182/0.757576;0.555556/0.757576;0.543478/0.757576;0.531915/0.787879;0.541667/0.818182;0.55102/0.818182;0.54/0.848485;0.54902/0.848485;0.538462/0.848485;0.528302/0.878788;0.537037/0.878788;0.527273/0.878788;0.517857/0.878788;0.508772/0.878788;0.5/0.878788;0.491525/0.878788;0.483333/0.909091;0.491803/0.939394;0.5/0.939394;0.492063/0.939394;0.484375/0.939394;0.476923/0.939394;0.469697/0.939394;0.462687/0.969697;0.470588/0.969697;0.463768/0.969697;0.457143/0.969697;0.450704/0.969697;0.444444/0.969697;0.438356/0.969697;0.432432/0.969697;0.426667/1;0.434211/1;0.428571/1;0.423077
TPR;PPV/0;1/0.022222;1/0.044444;1/0.066667;1/0.088889;1/0.111111;1/0.133333;1/0.155556;1/0.177778;1/0.177778;0.888889/0.2;0.9/0.2;0.818182/0.222222;0.833333/0.244444;0.846154/0.266667;0.857143/0.266667;0.8/0.288889;0.8125/0.311111;0.823529/0.333333;0.833333/0.333333;0.789474/0.355556;0.8/0.377778;0.809524/0.4;0.818182/0.422222;0.826087/0.422222;0.791667/0.444444;0.8/0.466667;0.807692/0.488889;0.814815/0.511111;0.821429/0.533333;0.827586/0.555556;0.833333/0.577778;0.83871/0.577778;0.8125/0.6;0.818182/0.622222;0.823529/0.622222;0.8/0.644444;0.805556/0.644444;0.783784/0.644444;0.763158/0.666667;0.769231/0.688889;0.775/0.688889;0.756098/0.688889;0.738095/0.711111;0.744186/0.733333;0.75/0.755556;0.755556/0.777778;0.76087/0.777778;0.744681/0.777778;0.729167/0.777778;0.714286/0.8;0.72/0.8;0.705882/0.822222;0.711538/0.844444;0.716981/0.866667;0.722222/0.888889;0.727273/0.888889;0.714286/0.911111;0.719298/0.911111;0.706897/0.911111;0.694915/0.933333;0.7/0.933333;0.688525/0.955556;0.693548/0.955556;0.68254/0.955556;0.671875/0.955556;0.661538/0.977778;0.666667/0.977778;0.656716/0.977778;0.647059/0.977778;0.637681/0.977778;0.628571/0.977778;0.619718/0.977778;0.611111/0.977778;0.60274/0.977778;0.594595/1;0.6/1;0.592105/1;0.584416/1;0.576923
TPR;PPV/0;1/0.030303;1/0.060606;1/0.090909;1/0.090909;0.75/0.121212;0.8/0.151515;0.833333/0.181818;0.857143/0.212121;0.875/0.242424;0.888889/0.272727;0.9/0.30303;0.909091/0.333333;0.916667/0.333333;0.846154/0.363636;0.857143/0.393939;0.866667/0.424242;0.875/0.424242;0.823529/0.454545;0.833333/0.454545;0.789474/0.484848;0.8/0.515152;0.809524/0.515152;0.772727/0.545455;0.782609/0.545455;0.75/0.545455;0.72/0.545455;0.692308/0.545455;0.666667/0.575758;0.678571/0.575758;0.655172/0.606061;0.666667/0.636364;0.677419/0.666667;0.6875/0.666667;0.666667/0.666667;0.647059/0.666667;0.628571/0.666667;0.611111/0.69697;0.621622/0.727273;0.631579/0.727273;0.615385/0.727273;0.6/0.757576;0.609756/0.787879;0.619048/0.787879;0.604651/0.818182;0.613636/0.818182;0.6/0.818182;0.586957/0.848485;0.595745/0.848485;0.583333/0.848485;0.571429/0.848485;0.56/0.848485;0.54902/0.848485;0.538462/0.848485;0.528302/0.848485;0.518519/0.878788;0.527273/0.878788;0.517857/0.878788;0.508772/0.878788;0.5/0.878788;0.491525/0.909091;0.5/0.909091;0.491803/0.909091;0.483871/0.909091;0.47619/0.939394;0.484375/0.939394;0.476923/0.939394;0.469697/0.939394;0.462687/0.969697;0.470588/0.969697;0.463768/1;0.471429/1;0.464789/1;0.458333/1;0.452055/1;0.445946/1;0.44/1;0.434211/1;0.428571/1;0.423077
TPR;PPV/0;1/0.022222;1/0.044444;1/0.066667;1/0.088889;1/0.111111;1/0.133333;1/0.155556;1/0.177778;1/0.2;1/0.222222;1/0.244444;1/0.266667;1/0.288889;1/0.311111;1/0.333333;1/0.333333;0.9375/0.355556;0.941176/0.377778;0.944444/0.4;0.947368/0.422222;0.95/0.422222;0.904762/0.444444;0.909091/0.466667;0.913043/0.488889;0.916667/0.488889;0.88/0.511111;0.884615/0.533333;0.888889/0.555556;0.892857/0.555556;0.862069/0.577778;0.866667/0.577778;0.83871/0.6;0.84375/0.622222;0.848485/0.622222;0.823529/0.644444;0.828571/0.666667;0.833333/0.666667;0.810811/0.688889;0.815789/0.688889;0.794872/0.711111;0.8/0.711111;0.780488/0.711111;0.761905/0.711111;0.744186/0.711111;0.727273/0.733333;0.733333/0.755556;0.73913/0.755556;0.723404/0.777778;0.729167/0.777778;0.714286/0.8;0.72/0.8;0.705882/0.822222;0.711538/0.822222;0.698113/0.822222;0.685185/0.822222;0.672727/0.844444;0.678571/0.866667;0.684211/0.888889;0.689655/0.911111;0.694915/0.911111;0.683333/0.911111;0.672131/0.933333;0.677419/0.933333;0.666667/0.933333;0.65625/0.955556;0.661538/0.955556;0.651515/0.955556;0.641791/0.977778;0.647059/0.977778;0.637681/1;0.642857/1;0.633803/1;0.625/1;0.616438/1;0.608108/1;0.6/1;0.592105/1;0.584416/1;0.576923
TPR;PPV/0;1/0.030303;1/0.060606;1/0.090909;1/0.121212;1/0.151515;1/0.181818;1/0.212121;1/0.242424;1/0.242424;0.888889/0.272727;0.9/0.272727;0.818182/0.30303;0.833333/0.333333;0.846154/0.333333;0.785714/0.363636;0.8/0.393939;0.8125/0.393939;0.764706/0.424242;0.777778/0.454545;0.789474/0.454545;0.75/0.454545;0.714286/0.454545;0.681818/0.454545;0.652174/0.484848;0.666667/0.515152;0.68/0.545455;0.692308/0.545455;0.666667/0.575758;0.678571/0.575758;0.655172/0.606061;0.666667/0.606061;0.645161/0.636364;0.65625/0.636364;0.636364/0.636364;0.617647/0.666667;0.628571/0.69697;0.638889/0.727273;0.648649/0.757576;0.657895/0.757576;0.641026/0.787879;0.65/0.787879;0.634146/0.818182;0.642857/0.818182;0.627907/0.818182;0.613636/0.848485;0.622222/0.848485;0.608696/0.848485;0.595745/0.878788;0.604167/0.878788;0.591837/0.909091;0.6/0.909091;0.588235/0.909091;0.576923/0.909091;0.566038/0.939394;0.574074/0.939394;0.563636/0.939394;0.553571/0.939394;0.54386/0.969697;0.551724/0.969697;0.542373/0.969697;0.533333/0.969697;0.52459/0.969697;0.516129/1;0.52381/1;0.515625/1;0.507692/1;0.5/1;0.492537/1;0.485294/1;0.478261/1;0.471429/1;0.464789/1;0.458333/1;0.452055/1;0.445946/1;0.44/1;0.434211/1;0.428571/1;0.423077
";

            var p11 = false;
            var average_for_closest_points = true;
            var open_bmp = true;

            plot_curve(perf_curve_types.roc, encoded_roc, $@"c:\svm_compute\charts\{nameof(perf_curve_types.roc)}.bmp", p11, average_for_closest_points, open_bmp);
            plot_curve(perf_curve_types.pr, encoded_pr, $@"c:\svm_compute\charts\{nameof(perf_curve_types.pr)}.bmp", p11, average_for_closest_points, open_bmp);
            plot_curve(perf_curve_types.pri_from_pr, encoded_pr, $@"c:\svm_compute\charts\{nameof(perf_curve_types.pri_from_pr)}.bmp", p11, average_for_closest_points, open_bmp);

            p11 = true;
            plot_curve(perf_curve_types.roc, encoded_roc, $@"c:\svm_compute\charts\{nameof(perf_curve_types.roc)}{(p11 ? "_p11" : "")}.bmp", p11, average_for_closest_points, open_bmp);
            plot_curve(perf_curve_types.pr, encoded_pr, $@"c:\svm_compute\charts\{nameof(perf_curve_types.pr)}{(p11 ? "_p11" : "")}.bmp", p11, average_for_closest_points, open_bmp);
            plot_curve(perf_curve_types.pri_from_pr, encoded_pr, $@"c:\svm_compute\charts\{nameof(perf_curve_types.pri_from_pr)}{(p11 ? "_p11" : "")}.bmp", p11, average_for_closest_points, open_bmp);

        }

        public enum perf_curve_types : int
        {
            pr = 1,
            pri = 2,
            pri_from_pr = 3,
            roc = 4
        }

        public static double scale(double value, double min, double max, double min_scale, double max_scale)
        {
            return (max_scale - min_scale) * ((value - min) / (max - min)) + min_scale;
        }

        public static double area_under_curve_trapz(List<(double x, double y)> coordinate_list)
        {
            coordinate_list = coordinate_list.Distinct().ToList();
            coordinate_list = coordinate_list.OrderBy(a => a.x).ThenBy(a => a.y).ToList();
            var auc = coordinate_list.Select((c, i) => i >= coordinate_list.Count - 1 ? 0 : (coordinate_list[i + 1].x - coordinate_list[i].x) * ((coordinate_list[i].y + coordinate_list[i + 1].y) / 2)).Sum();
            return auc;
        }

        public static void plot_curve(perf_curve_types perf_curve_type, string encoded_plot_data, string save_filename, bool convert_to_p11 = false, bool average_for_closest_points = true, bool open_bmp = true)
        {
            // the data is in the format of x_title;y_title/x;y/x;y/x;y/...
            // each line represents a different curve.  usually from outer-cross-validation, there would be 5 or 10 curves (although it could be any number >= 2).  if the input the average value from cross-validation, there would only be one curve.
            // note, it is not necessary that the number of values is equal per curve.  however, they should all be x,y values of either ROC or PR curves - other types of curves are not necessarily supported as there is code here specific to ROC and PR.

            if (perf_curve_type == 0)
            {
                throw new ArgumentException("Invalid performance curve type specified.", nameof(perf_curve_type));
            }

            if (string.IsNullOrWhiteSpace(save_filename))
            {
                throw new ArgumentException("BMP filename required to save curve.", nameof(save_filename));
            }

            if (string.IsNullOrWhiteSpace(encoded_plot_data))
            {
                throw new ArgumentException("Plot x,y coordinate data is missing.", nameof(encoded_plot_data));
            }

            List<(string x_title, string y_title, List<(double x, double y)> xy)> parsed_xy = encoded_plot_data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(line =>
                {
                    var headers = line.Trim().Split('/').First().Split(';');

                    var r = (x_title: headers[0], y_title: headers[1], xy: line.Trim().Split('/').Skip(1).Select(b => (x: double.Parse("" + b.Split(';')[0]), y: double.Parse("" + b.Split(';')[1]))).ToList());

                    r.xy = r.xy.OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).ToList();

                    return r;

                }).ToList();



            if (convert_to_p11)
            {
                // convert the x,y coordinate to the 11 point thresholds (useful for comparison of different classification models, where thresholds are unlikely to be meaningful)
                var points11 = new[] { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };

                for (var index = 0; index < parsed_xy.Count; index++)
                {

                    if (perf_curve_type == perf_curve_types.roc)
                    {
                        parsed_xy[index] = (parsed_xy[index].x_title, parsed_xy[index].y_title, points11.Select(t => (t, parsed_xy[index].xy.Where(a => a.x <= t).Max(a => a.y))).ToList());
                    }
                    else
                    {
                        parsed_xy[index] = (parsed_xy[index].x_title, parsed_xy[index].y_title, points11.Select(t => (t, parsed_xy[index].xy.Where(a => a.x >= t).Max(a => a.y))).ToList());

                    }
                }
            }

            if (perf_curve_type == perf_curve_types.pri_from_pr)
            {
                // interpolate the x,y values (also for comparison of classifiation models)
                var last_y = 0d;

                parsed_xy = parsed_xy.Select(a => (x_title: a.x_title, y_title: a.y_title, xy: a.xy.Select((b, i) =>
                {
                    //x = TPR; y = PPV

                    var max_ppv = a.xy.Where(c => c.x >= b.x).Max(c => c.y);

                    if (double.IsNaN(max_ppv)) max_ppv = last_y;

                    last_y = max_ppv;

                    return (x: b.x, y: max_ppv);
                }).ToList())).ToList();
            }

            (string x_title, string y_title, List<(double x, double y)> xy) average_xy2 = (parsed_xy.First().x_title, parsed_xy.First().y_title, new List<(double x, double y)>());

            if (average_for_closest_points)
            {
                // make average curve for all outer-cv curves

                var all_xy = parsed_xy.SelectMany(a => a.xy).OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).Distinct().ToList();
                var closest_to_all_xy = all_xy.Select(z => parsed_xy.Select(a => a.xy.OrderBy(b => Math.Abs(b.x - z.x) + Math.Abs(b.y - z.y)).First()).ToList()).ToList();

                average_xy2.xy = closest_to_all_xy.Select(a => (x: a.Average(b => b.x), y: a.Average(b => b.y))).Distinct().OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).ToList();

                average_xy2.xy = average_xy2.xy.Distinct().OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).ToList();
            }

            List<(string x_title, string y_title, List<(double x, double y)> xy)> data = parsed_xy;

            var average_curves = 0;
            if (average_xy2.xy != null && average_xy2.xy.Count > 0) { data.Add(average_xy2); average_curves++; }

            var graph_title = perf_curve_type.ToString().Replace("_", " ").ToUpperInvariant() + " " + string.Join(" ", string.Join("|", data.Select(a => a.x_title + "/" + a.y_title).Distinct().ToList()), "Curve");
            var axis_x_title = string.Join(" ", string.Join("|", data.Select(a => a.x_title).Distinct().ToList()), "(X)");
            var axis_y_title = string.Join(" ", string.Join("|", data.Select(a => a.y_title).Distinct().ToList()), "(Y)");

            var width_x = 2000; // could be specified as a parameter
            var height_y = 2000; // could be specified as a parameter

            var bg_color = Color.Transparent;
            var fg_color = Color.White; // not in use - using transparent instead
            var grid_color = Color.LightSlateGray;
            var title_color = Color.White;

            var axis_start_color = Color.Blue;
            var axis_end_color = Color.Red;

            var bmp = new Bitmap(width_x, height_y);

            var graph_width_x = (double)bmp.Width * 0.8; // could be specified as a parameter
            var graph_height_y = (double)bmp.Height * 0.8; // could be specified as a parameter

            var graph_start_x = ((double)width_x - (double)graph_width_x) / (double)2;
            var graph_end_x = (double)graph_start_x + (double)graph_width_x;
            var graph_start_y = ((double)height_y - (double)graph_height_y) / (double)2;
            var graph_end_y = (double)graph_start_y + (double)graph_height_y;

            program.WriteLine($@"{nameof(graph_start_x)}={graph_start_x}, {nameof(graph_start_y)}={graph_start_y}, {nameof(graph_end_x)}={graph_end_x}, {nameof(graph_end_y)}={graph_end_y}");

            using (var bmp_gfx = Graphics.FromImage(bmp))
            {
                /////////////////////////////////////////////////////////
                // set bg colour, and other gfx parameters
                bmp_gfx.Clear(bg_color);
                bmp_gfx.SmoothingMode = SmoothingMode.AntiAlias;
                bmp_gfx.InterpolationMode = InterpolationMode.HighQualityBicubic;
                bmp_gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
                bmp_gfx.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

                StringFormat format = new StringFormat()
                {
                    Alignment = StringAlignment.Center,
                    LineAlignment = StringAlignment.Center,
                };

                /////////////////////////////////////////////////////////
                // draw title
                {
                    var title_font = new Font("Tahoma", (int)graph_start_y / 4, GraphicsUnit.Pixel);
                    var title_brush = new SolidBrush(title_color);
                    bmp_gfx.DrawString(graph_title, title_font, title_brush, new RectangleF(0, 0, bmp.Width, (int)graph_start_y), format);
                }

                /////////////////////////////////////////////////////////
                // draw diagonal graph grid lines
                {
                    bmp_gfx.DrawLine(new Pen(grid_color, 2), (float)graph_start_x, (float)graph_start_y, (float)graph_end_x, (float)graph_end_y);
                    bmp_gfx.DrawLine(new Pen(grid_color, 2), (float)graph_start_x, (float)graph_end_y, (float)graph_end_x, (float)graph_start_y);
                }

                /////////////////////////////////////////////////////////
                // draw x axis title
                {

                    var axis_x_font = new Font("Tahoma", (int)((height_y - graph_end_y) / 4), GraphicsUnit.Pixel);
                    var axis_x_title_size = bmp_gfx.MeasureString(axis_x_title, axis_x_font);

                    var axis_x_title_start_x = (float)(((double)width_x / (double)2) - ((double)axis_x_title_size.Width / (double)2));
                    var axis_x_title_start_y = (float)((double)height_y - ((double)axis_x_title_size.Height));

                    var axis_x_brush = new LinearGradientBrush(new RectangleF(axis_x_title_start_x, axis_x_title_start_y, axis_x_title_size.Width, axis_x_title_size.Height), axis_start_color, axis_end_color, LinearGradientMode.Horizontal);

                    bmp_gfx.DrawString(axis_x_title, axis_x_font, axis_x_brush, axis_x_title_start_x, axis_x_title_start_y);
                }

                /////////////////////////////////////////////////////////
                // draw y axis title
                {
                    var axis_y_font = new Font("Tahoma", (int)graph_start_x / 4, GraphicsUnit.Pixel);
                    var axis_y_title_size = bmp_gfx.MeasureString(axis_y_title, axis_y_font); //, new StringFormat(StringFormatFlags.DirectionVertical));
                    var axis_y_title_start_x = (float)0; // (axis_y_title_size.Height/2);
                    var axis_y_title_start_y = (float)((bmp.Height / 2) + (axis_y_title_size.Width / 2));
                    var axis_y_title_end_x = axis_y_title_start_x + axis_y_title_size.Height;
                    var axis_y_title_end_y = axis_y_title_start_y + axis_y_title_size.Width;
                    bmp_gfx.TranslateTransform(axis_y_title_start_x, axis_y_title_start_y);
                    bmp_gfx.RotateTransform(90 * 3);
                    var axis_y_brush = new LinearGradientBrush(new RectangleF(0, 0, axis_y_title_size.Width, axis_y_title_size.Height), axis_start_color, axis_end_color, LinearGradientMode.Horizontal);
                    bmp_gfx.DrawString(axis_y_title, axis_y_font, (Brush)axis_y_brush, 0, 0);
                    bmp_gfx.ResetTransform();
                }


                /////////////////////////////////////////////////////////
                // draw grid lines horizonal (x changes, y in min to max)
                {
                    var ticks_axis_x_brush = new LinearGradientBrush(new Rectangle((int)0, (int)0, (int)bmp.Width, bmp.Height), axis_start_color, axis_end_color, LinearGradientMode.Horizontal);
                    var ticks_axis_x_font = new Font("Tahoma", (int)((height_y - graph_end_y) / 4), GraphicsUnit.Pixel);

                    for (var x = 0.0; x <= 1.0; x += 0.1)
                    {
                        var x1 = scale(x, 0.0, 1.0, graph_start_x, graph_end_x);
                        var x2 = scale(x, 0.0, 1.0, graph_start_x, graph_end_x);

                        var y1 = scale(0.0, 0.0, 1.0, graph_start_y, graph_end_y);
                        var y2 = scale(1.0, 0.0, 1.0, graph_start_y, graph_end_y);


                        program.WriteLine($@"{nameof(x1)}={x1}, {nameof(y1)}={y1}, {nameof(x2)}={x2}, {nameof(y2)}={y2}");

                        bmp_gfx.DrawLine(new Pen(grid_color, 2), (float)x1, (float)y1, (float)x2, (float)y2);

                        var tick_text_size = bmp_gfx.MeasureString(x.ToString("0.0"), ticks_axis_x_font); // swap width and height, since it is rotated to side
                        bmp_gfx.TranslateTransform((float)x1 - (tick_text_size.Height / 2), (float)(graph_end_y + (tick_text_size.Width * 1.1)));
                        bmp_gfx.RotateTransform(90 * 3);
                        bmp_gfx.DrawString(x.ToString("0.0"), ticks_axis_x_font, ticks_axis_x_brush, 0, 0); // tick_text_size.Height);//, new StringFormat(StringFormatFlags.DirectionVertical));
                        bmp_gfx.ResetTransform();
                    }
                }

                /////////////////////////////////////////////////////////
                // draw grid lines vertical (y changes, x is min to max)
                {
                    var ticks_axis_y_brush = new LinearGradientBrush(new Rectangle((int)0, (int)0, (int)bmp.Width, bmp.Height), axis_start_color, axis_end_color, LinearGradientMode.Horizontal);
                    var ticks_axis_y_font = new Font("Tahoma", (int)((graph_start_x) / 4), GraphicsUnit.Pixel);

                    for (var y = 0.0; y <= 1.0; y += 0.1)
                    {
                        var x1 = scale(0.0, 0.0, 1.0, graph_start_x, graph_end_x);
                        var x2 = scale(1.0, 0.0, 1.0, graph_start_x, graph_end_x);

                        var y1 = scale(y, 0.0, 1.0, graph_start_y, graph_end_y);
                        var y2 = scale(y, 0.0, 1.0, graph_start_y, graph_end_y);

                        program.WriteLine($@"{nameof(x1)}={x1}, {nameof(y1)}={y1}, {nameof(x2)}={x2}, {nameof(y2)}={y2}");
                        bmp_gfx.DrawLine(new Pen(grid_color, 2), (float)x1, (float)y1, (float)x2, (float)y2);


                        var tick_text_size = bmp_gfx.MeasureString((1 - y).ToString("0.0"), ticks_axis_y_font);
                        bmp_gfx.DrawString((1 - y).ToString("0.0"), ticks_axis_y_font, ticks_axis_y_brush, (float)(graph_start_x - (tick_text_size.Width * 1.0)), (float)(y2 - (tick_text_size.Height / 2)));

                    }
                }

                /////////////////////////////////////////////////////////
                // draw data
                var color_rnd = new Random(2);

                for (var i = 0; i < data.Count; i++)
                {
                    var is_average_curve = i >= data.Count - average_curves;

                    var c = is_average_curve ? Color.White : Color.FromArgb(color_rnd.Next(0, 255), color_rnd.Next(0, 255), color_rnd.Next(0, 255));

                    var auc = area_under_curve_trapz(data[i].xy);
                    auc = Math.Round(auc, 2);
                    var auc_str = "AUC " + auc.ToString("0.00");
                    var auc_str_font = new Font("Tahoma", (int)((graph_start_x) / 4), GraphicsUnit.Pixel);

                    var auc_str_size = bmp_gfx.MeasureString(auc_str, auc_str_font);

                    bmp_gfx.DrawString(auc_str, auc_str_font, new SolidBrush(c), (int)(width_x - auc_str_size.Width), (int)(graph_start_y + (auc_str_size.Height * i)));

                    for (var a = 1; a < data[i].xy.Count; a++)
                    {
                        var xy1 = data[i].xy[a - 1];
                        var xy2 = data[i].xy[a];

                        var x1 = scale(xy1.x, 0.0, 1.0, graph_start_x, graph_end_x);
                        var y1 = scale((1 - xy1.y), 0.0, 1.0, graph_start_y, graph_end_y);

                        var x2 = scale(xy2.x, 0.0, 1.0, graph_start_x, graph_end_x);
                        var y2 = scale((1 - xy2.y), 0.0, 1.0, graph_start_y, graph_end_y);

                        var point_size = 20;
                        var pen_size = 4;

                        bmp_gfx.DrawLine(new Pen(c, pen_size), (float)x1, (float)y1, (float)x2, (float)y2);

                        if (a == 1)
                        {
                            bmp_gfx.DrawEllipse(new Pen(c, pen_size), (int)x1 - (point_size / 2), (int)y1 - (point_size / 2), point_size, point_size);
                        }
                        bmp_gfx.DrawEllipse(new Pen(c, pen_size), (int)x2 - (point_size / 2), (int)y2 - (point_size / 2), point_size, point_size);
                    }
                }
            }

            Directory.CreateDirectory(Path.GetDirectoryName(save_filename));
            bmp.Save(save_filename, ImageFormat.Bmp);

            if (open_bmp)
            {
                Process.Start(save_filename);
            }
        }
    }
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

When drawing graphics I admit that it in general can be difficult to split the code into meaningful sub methods because often many variables are used across different sections of the process. A way to do it could be to make a non static class, that holds the entire data set, constant key measurements and other resources as member fields/properties and then split the huge method plot_curve(...) into meaningful sub methods.

At least I would extract the preprocessing of the input data and the postprocessing of the resulting bmp in to dedicated methods.


List<(double x, double y)

You use a named tuple as data type for points. Why not use the built in PointF instead. Or - if you want the double precision in calculations - make your own PointD data type - and then abstract the conversion to and from Point and PointF away in cast operators. I think that would make the entire code more readable. There are a lot of inline casting, that could be avoided this way, and you could use the Graphics.DrawXXX(...) overrides that take PointF arguments instead of x,y arguments.


  if (perf_curve_type == 0)
  {
    throw new ArgumentException("Invalid performance curve type specified.", nameof(perf_curve_type));
  }

You make this test, but what about values above 4? Should it even be necessary for an enum type?


  List<(string x_title, string y_title, List<(double x, double y)> xy)> parsed_xy = encoded_plot_data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
      .Select(line =>
      {
        var headers = line.Trim().Split('/').First().Split(';');
        var r = (x_title: headers[0], y_title: headers[1], xy: line.Trim().Split('/').Skip(1).Select(b => (x: double.Parse("" + b.Split(';')[0]), y: double.Parse("" + b.Split(';')[1]))).ToList());
        r.xy = r.xy.OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).ToList();
        return r;
      }).ToList();

This fails for the provided test input, because there is a whitespace line as the last line. You need to filter that out before the Select(line...) statement:

  CultureInfo parseFormat = CultureInfo.InvariantCulture;
  List<(string x_title, string y_title, List<(double x, double y)> xy)> parsed_xy = encoded_plot_data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
    .Where(line => !string.IsNullOrWhiteSpace(line)) 
    .Select(line =>
    {
      var headers = line.Trim().Split('/').First().Split(';');

      var r = (x_title: headers[0], y_title: headers[1], xy: line.Trim().Split('/').Skip(1).Select(b => (x: double.Parse("" + b.Split(';')[0], parseFormat), y: double.Parse("" + b.Split(';')[1], parseFormat))).ToList());

      r.xy = r.xy.OrderBy(a => a.x).ThenBy(a => perf_curve_type == perf_curve_types.roc ? a.y : 1 - a.y).ToList();
      return r;
    }).ToList();

As shown above you also need a proper format provider, when parsing the coordinates and other numbers in order to handle '.' and ',' correctly in respect to the file format.

Have you considered to use a Regex pattern instead?


.ToList()

You have a lot of these calls where you make a LINQ queries. For large data sets they might be expensive, because they create a copy of the data set. They should be unnecessary when just querying the set, so consider to avoid them as much as possible and only use them when you want to create a sub set reused more than once.

For instance:

public static double area_under_curve_trapz(List<(double x, double y)> coordinate_list)
{
  coordinate_list = coordinate_list.Distinct().ToList();
  coordinate_list = coordinate_list.OrderBy(a => a.x).ThenBy(a => a.y).ToList();
  var auc = coordinate_list.Select((c, i) => i >= coordinate_list.Count - 1 ? 0 : (coordinate_list[i + 1].x - coordinate_list[i].x) * ((coordinate_list[i].y + coordinate_list[i + 1].y) / 2)).Sum();
  return auc;
}

could be written as:

public static double area_under_curve_trapz(List<(double x, double y)> coordinate_list)
{
  var auc = coordinate_list
    .Distinct()
    .OrderBy(a => a.x)
    .ThenBy(a => a.y)
    .Select((c, i) => 
      i >= coordinate_list.Count - 1 
      ? 0 
      : (coordinate_list[i + 1].x - coordinate_list[i].x) * ((coordinate_list[i].y + coordinate_list[i + 1].y) / 2)).Sum();
  return auc;
}

It's just a query, that doesn't modify the source set, so it's safe to operate on that directly.


In general all graphic resources like pens, fonts, brushes etc as:

new SolidBrush(title_color)

should be disposed after use, so wrap them in a using statement.


new Font("Tahoma", (int)graph_start_y / 4, GraphicsUnit.Pixel) new Font("Tahoma", (int)graph_start_x / 4, GraphicsUnit.Pixel);

You repeatedly recreate these fonts, but graph_start_x and graph_start_y are constant throughout the method, so create them once


      for (var y = 0.0; y <= 1.0; y += 0.1)
      {
        var x1 = scale(0.0, 0.0, 1.0, graph_start_x, graph_end_x);
        var x2 = scale(1.0, 0.0, 1.0, graph_start_x, graph_end_x);
        ...

In some of the loops you instantiate x1, x2 or y1, y2 inside the loop although they are invariant while the loop is running. Consider to declare them before the loop:

      var x1 = scale(0.0, 0.0, 1.0, graph_start_x, graph_end_x); are constant => out of loop
      var x2 = scale(1.0, 0.0, 1.0, graph_start_x, graph_end_x);
      for (var y = 0.0; y <= 1.0; y += 0.1)
      {
        ...

All in all I think the output is quite nice. I would make some adjustments in font sizes and the placement of the AUC results, and I wouldn't rotate the values on the x-axis.

Consider to use a bitmap format that is compressed - for instance png. The produced output in bmp format has a size of about 15 MB where a png only takes < 1 MB.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.