001    package com.softnetConsult.utils.math;
002    
003    import java.util.Collections;
004    import java.util.Comparator;
005    import java.util.Iterator;
006    import java.util.Set;
007    import java.util.TreeMap;
008    import java.util.NavigableMap;
009    import java.util.Map.Entry;
010    
011    import com.softnetConsult.utils.collections.Pair;
012    
013    
014    /**
015     * This class implements a sample distribution; it is a useful tool when undertaking a
016     * statistical analysis of value producing processes: it counts the number of times each
017     * value is observed (method {@link #observe(Number)}) and can be used conveniently for
018     * obtaining the number of observations within a certain range, discretising the observed
019     * sample, calculating mean, variance and other sample properties, printing and plotting
020     * the distribution, and so on.<br />
021     * Where possible, the methods provided by the class type of the sample values are used
022     * directly, for other computations the values are converted to {@code double}; be careful
023     * as this might have consequences if the type of the sampled variable cannot be exactly
024     * converted to {@code double}.  
025     * 
026     * <p style="font-size:smaller;">This product includes software developed by the
027     *    <strong>SoftNet-Consult Java Utility Library</strong> project and its contributors.<br />
028     *    (<a href="http://java-tools.sourceforge.net" target="_blank">http://java-tools.sourceforge.net</a>)<br />
029     *    Copyright (c) 2007-2008 SoftNet-Consult.<br />
030     *    Copyright (c) 2007-2008 G. Paperin.<br />
031     *    All rights reserved.
032     * </p>
033     * <p style="font-size:smaller;">File: Distribution.java<br />
034     *    Library API version: {@value com.softnetConsult.utils.APIProperties#apiVersion}<br />
035     *    Java compliance version: {@value com.softnetConsult.utils.APIProperties#javaComplianceVersion}
036     * </p>
037     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
038     *    without modification, are permitted provided that the following terms and conditions are met:
039     * </p>
040     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
041     *    acknowledgement of the SoftNet-Consult Java Utility Library project, the above copyright
042     *    notice, this list of conditions and the following disclaimer.<br />
043     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
044     *    SoftNet-Consult Java Utility Library project, the above copyright notice, this list of
045     *    conditions and the following disclaimer in the documentation and/or other materials
046     *    provided with the distribution.<br />
047     *    3. All advertising materials mentioning features or use of this software or any derived
048     *    software must display the following acknowledgement:<br />
049     *    <em>This product includes software developed by the SoftNet-Consult Java Utility Library
050     *    project and its contributors.</em>
051     * </p>
052     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
053     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
054     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
055     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
056     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
057     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
058     * </p> 
059     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
060     * @version {@value com.softnetConsult.utils.APIProperties#apiVersion}
061     *
062     * @param <T> The type of values of which the distribution is being analysed.
063     */
064    public class Distribution<T extends Number> {
065    
066    /**
067     * Stores the observed sample (MAP: observation -> number of times observed).
068     */
069    private TreeMap<T, Integer> dist;
070    
071    /**
072     * Smallest observation in this sample.
073     */
074    private T minObservation;
075    
076    /**
077     * Largest observation in this sample.
078     */
079    private T maxObservation;
080    
081    /**
082     * Number of observations in this sample.
083     */
084    private long observationCount;
085    
086    /**
087     * Sample mean.
088     */
089    private double mean;
090    
091    /**
092     * Sample variance.
093     */
094    private double variance;
095    
096    
097    /**
098     * A comparator object for all {@code Number} types. This comparator uses
099     * {@link MathTools#compare(Number, Number)} for the camparison.
100     */
101    private static final Comparator<Number> comparator = new Comparator<Number>() {
102            public int compare(Number x, Number y) {
103                    return MathTools.compare(x, y);
104            }
105    };
106    
107    private static String newLine = null;
108    private static String getNewLine() {
109            if (null == newLine)
110                    newLine = String.format("%n");
111            return newLine;
112    }
113    
114    
115    /**
116     * Creates a new empty distribution sample.
117     */
118    public Distribution() {
119            this.dist = new TreeMap<T, Integer>(comparator);
120            this.minObservation = null;
121            this.maxObservation = null;
122            this.observationCount = 0L;
123            this.mean = Double.NaN;
124            this.variance = Double.NaN;
125    }
126    
127    
128    /**
129     * Used internally for creating discretised sammples and sub-samples of this distribution.
130     * 
131     * @param dist The map containg the observation of this distribution.
132     */
133    private Distribution(TreeMap<T, Integer> dist) {
134            this.dist = dist;
135            if (dist.isEmpty()) {
136                    this.minObservation = null;
137                    this.maxObservation = null;
138                    this.observationCount = 0L;
139            } else {
140                    this.minObservation = dist.firstKey();
141                    this.maxObservation = dist.lastKey();
142                    this.observationCount = 0L;
143                    for (Integer count : dist.values())
144                            this.observationCount += count.intValue();
145            }
146            this.mean = Double.NaN;
147            this.variance = Double.NaN;
148    }
149    
150    /**
151     * Adds an observation to this sample.
152     * 
153     * @param observation The observed value.
154     */
155    public void observe(T observation) {
156            
157            if (null == observation)
158                    throw new NullPointerException("null observation is not allowed");
159            
160            if (dist.containsKey(observation)) {
161                    dist.put(observation, dist.get(observation) + 1);
162            } else {
163                    dist.put(observation, 1);
164            }
165            
166            observationCount++;
167            mean = Double.NaN;
168            variance = Double.NaN;
169            
170            if (minObservation == null || MathTools.compare(observation, minObservation) < 0)
171                    minObservation = observation;
172            
173            if (maxObservation == null || MathTools.compare(observation, maxObservation) > 0)
174                    maxObservation = observation;
175    }
176    
177    
178    /**
179     * Discretises this sample into intervals of the specified length. This is equivalent to
180     * {@code discretise(getMin(), getMax(), interval)}.
181     * 
182     * @param interval The size of the discrete intervals.
183     * @return A discretised distribution containing the same observations as this distribution.
184     * @see #discretise(Number, Number, double)
185     */
186    public Distribution<Double> discretise(double interval) {
187            return discretise(minObservation, maxObservation, interval);    
188    }
189    
190    
191    /**
192     * Discretises this sample into intervals of the specified length while only considering
193     * the observations between {@code min} and {@code max} (inclusive).<br />
194     * <br />
195     * If {@code S = (max - min) / interval}, the resulting distribution will contain {@code S}
196     * observations if {@code (max - min)} is not exactly dividable by {@code interval}, otherwise
197     * it will contain {@code S + 1} observations.<br />
198     * The first observation of the resulting distrinution will be {@code min} and the
199     * corresponding frequency will be the sum of the frequencies of all observations between
200     * {@code min} (inclusive) and {@code min + inverval} (exclusive) in this original distribution.<br />
201     * The n-th observation of the resulting distrinution will be {@code (min + (n-1) * interval} and
202     * the corresponding frequency will be the sum of the frequencies of all observations between
203     * {@code min + (n-1) * inverval} (inclusive) and {@code min + n * inverval} (exclusive) in this
204     * original distribution. No observations of this original distribution that are larger than
205     * the specified {@code max} will be considered.<br />
206     * <br />
207     * If {@code min} > {@code max} the resulting distribution will be empty.<br />
208     * This method converts the observations contained in this sample to {@code double}; be careful
209     * as this might have consequences if the type of the values in this sample cannot be exactly
210     * converted to {@code double}. Be also aware of the "{@code 1 + 1 = 1.9999999}" effect which may
211     * cause inconsistencies between this and disretised distribution. 
212     * 
213     * @param min The minimum of the observation interval to be discretised. 
214     * @param max The maximum of the observation interval to be discretised.
215     * @param interval The size of the discrete intervals. 
216     * @return A discretised sample as described above.
217     */
218    public Distribution<Double> discretise(T min, T max, double interval) {
219            
220            if (interval <= 0.0)
221                    throw new IllegalArgumentException("interval <= 0.0 is not allowed");
222            
223            TreeMap<Double, Integer> discreteDist = new TreeMap<Double, Integer>(comparator);
224            NavigableMap<T, Integer> view = dist.subMap(min, true, max, true);
225            double maxD = max.doubleValue();
226            
227            double group = min.doubleValue();
228            while (group <= maxD) {                              
229                    int count = 0;  
230                    boolean done = false;
231                    Iterator<Entry<T, Integer>> it = view.entrySet().iterator();
232                    Entry<T, Integer> entry;
233                    double observ;
234                    while(it.hasNext() && !done) {
235                            entry = it.next();
236                            observ = entry.getKey().doubleValue();
237                            done = group + interval <= observ;
238                            if (group <= observ && !done)
239                                    count += entry.getValue().intValue();                   
240                    }               
241                    discreteDist.put(group, count);
242                    
243                    group += interval;
244            }
245            
246            return new Distribution<Double>(discreteDist);
247    }
248    
249    /**
250     * Discretises this sample into intervals of the specified length. This is equivalent to
251     * {@code discretise(getMin(), getMax(), interval)}.
252     * 
253     * @param interval The size of the discrete intervals.
254     * @return A discretised distribution containing the same observations as this distribution.
255     * @see #discretise(Number, Number, int)
256     */
257    public Distribution<Integer> discretise(int interval) {
258            return discretise(minObservation, maxObservation, interval);
259    }
260    
261    /**
262     * Discretises this sample into intervals of the specified length while only considering
263     * the observations between {@code min} and {@code max} (inclusive).<br />
264     * <br />
265     * If {@code S = (max - min) / interval}, the resulting distribution will contain {@code S}
266     * observations if {@code (max - min)} is not exactly dividable by {@code interval}, otherwise
267     * it will contain {@code S + 1} observations.<br />
268     * The first observation of the resulting distrinution will be {@code min} and the
269     * corresponding frequency will be the sum of the frequencies of all observations between
270     * {@code min} (inclusive) and {@code min + inverval} (exclusive) in this original distribution.<br />
271     * The n-th observation of the resulting distrinution will be {@code (min + (n-1) * interval} and
272     * the corresponding frequency will be the sum of the frequencies of all observations between
273     * {@code min + (n-1) * inverval} (inclusive) and {@code min + n * inverval} (exclusive) in this
274     * original distribution. No observations of this original distribution that are larger than
275     * the specified {@code max} will be considered.<br />
276     * <br />
277     * If {@code min} > {@code max} the resulting distribution will be empty.<br />
278     * This method converts the observations contained in this sample to {@code int}; be careful
279     * as this might have consequences if the type of the values in this sample cannot be exactly
280     * converted to {@code int}. 
281     * 
282     * @param min The minimum of the observation interval to be discretised. 
283     * @param max The maximum of the observation interval to be discretised.
284     * @param interval The size of the discrete intervals. 
285     * @return A discretised sample as described above.
286     */
287    public Distribution<Integer> discretise(T min, T max, int interval) {
288            
289            if (interval <= 0)
290                    throw new IllegalArgumentException("interval <= 0 is not allowed");
291            
292            TreeMap<Integer, Integer> discreteDist = new TreeMap<Integer, Integer>(comparator);
293            NavigableMap<T, Integer> view = dist.subMap(min, true, max, true);
294            int maxI = max.intValue();
295            
296            int group = min.intValue();
297            while (group <= maxI) {              
298                    int count = 0;
299                    boolean done = false;
300                    Iterator<Entry<T, Integer>> it = view.entrySet().iterator();
301                    Entry<T, Integer> entry;
302                    int observ;
303                    while(it.hasNext() && !done) {
304                            entry = it.next();
305                            observ = entry.getKey().intValue();
306                            done = group + interval <= observ;
307                            if (group <= observ && !done)
308                                    count += entry.getValue().intValue();                   
309                    }
310                    discreteDist.put(group, count);
311                    
312                    group += interval;
313            }
314            return new Distribution<Integer>(discreteDist);
315    }
316    
317    /**
318     * A sample that contains only the values of this sample between the specified boundaries.
319     *  
320     * @param min Min observation.
321     * @param max Max observation.
322     * @return A new {@code Distribution} that is the same as this distribution, but contains
323     * only observations between {@code min} and {@code max} (inclusive). 
324     */
325    public Distribution<T> selectInterval(T min, T max) {
326                    
327            if (null == min || null == max)
328                    throw new NullPointerException("min and max may not be null");
329            
330            NavigableMap<T, Integer> view = dist.subMap(min, true, max, true);
331            TreeMap<T, Integer> intervalDist = new TreeMap<T, Integer>(view);   
332            return new Distribution<T>(intervalDist);
333    }
334    
335    /**
336     * Gets the number of times the specified observation was encountered.
337     * 
338     * @param observation An observation value.
339     * @return The frequency of the speciefied observation.
340     */
341    public int countObservations(T observation) {
342            Integer count = dist.get(observation);
343            if (null == count)
344                    return 0;
345            return count.intValue();
346    }
347    
348    /**
349     * Gets the proportion of the specified observation out of all observations in this sample.
350     * 
351     * @param observation An observation value.
352     * @return The frequency of the speciefied observation divided by the size of this sample.
353     */
354    public double countProportion(T observation) {
355            if (0L == observationCount)
356                    return Double.NaN;
357            int count = countObservations(observation);
358            if (0 == count)
359                    return 0.0;
360            return count / (double) observationCount;
361    }
362    
363    /**
364     * Gets the total number of times that any observation within the specified range was encountered.
365     * 
366     * @param min Min observation.
367     * @param max Max observation.
368     * @return The sum of the frequencies of all observations between {@code min}
369     * and {@code max} (inclusive).
370     */
371    public int countObservations(T min, T max) {
372            if (null == min || null == max)
373                    throw new NullPointerException("min and max may not be null");
374            
375            NavigableMap<T, Integer> view = dist.subMap(min, true, max, true);
376            int count = 0;
377            for (Integer c : view.values()) {
378                    count += c.intValue();          
379            }
380            return count;
381    }
382    
383    /**
384     * Gets proportion of observations within the specified range out of all observations in this sample.
385     * 
386     * @param min Min observation.
387     * @param max Max observation.
388     * @return The sum of the frequencies of all observations between {@code min} and {@code max}
389     * (inclusive) divided by the number of observations in this sample.
390     */
391    public double countProportion(T min, T max) {
392            if (0L == observationCount)
393                    return Double.NaN;
394            int count = countObservations(min, max);
395            if (0 == count)
396                    return 0.0;
397            return count / (double) observationCount;
398    }
399    
400    /**
401     * Returns an unmodifiable {@code Set} view of the observations contained in this distribution sample.
402     * The set's iterator returns the keys in ascending order. The set is backed by the distributioon,
403     * so changes to the map are reflected in the set. If the map is modified while an iteration over
404     * the set is in progress, the results of the iteration are undefined.
405     * 
406     * @return An unmodifiable {@code Set} view of the observations contained in this distribution sample.
407     */
408    public Set<T> getObservations() {
409            return Collections.unmodifiableSet(dist.keySet());
410    }
411    
412    /**
413     * Computes the mean of this sample.
414     * This method converts the observations contained in this sample to {@code double} values;
415     * be careful as this might have consequences if the type of the values in this sample cannot
416     * be exactly converted to {@code double}.
417     * 
418     * @return The mean of this sample.
419     */
420    public double getMean() {
421            if (!Double.isNaN(mean))
422                    return mean;
423            
424            if (1L > observationCount)
425                    return Double.NaN;
426            
427            mean = 0.0;
428            for (Entry<T, Integer> entry : dist.entrySet()) {
429                    mean += entry.getKey().doubleValue() * entry.getValue().doubleValue();
430            }
431            mean /= observationCount;
432            return mean;
433    }
434    
435    
436    /**
437     * Computes the variance of this sample.
438     * This method converts the observations contained in this sample to {@code double} values;
439     * be careful as this might have consequences if the type of the values in this sample cannot
440     * be exactly converted to {@code double}.
441     * 
442     * @return The variance of this sample.
443     */
444    public double getVariance() {
445            if (!Double.isNaN(variance))
446                    return variance;
447            
448            if (2L > observationCount)
449                    return Double.NaN;
450            
451            double m = getMean();
452            variance = 0.0;
453            for (Entry<T, Integer> entry : dist.entrySet()) {
454                    double d = (entry.getKey().doubleValue() - m);
455                    variance += d * d * entry.getValue().doubleValue();
456            }
457            variance /= (observationCount - 1);
458            return variance;
459    }
460    
461    
462    /**
463     * Returns a new Distribution in which each observation frequency equals to the observation frequency
464     * of this distribution divided by the specified value;
465     * all resulting non-integer frequencies are rounded to the nearest integer.
466     * 
467     * @param value A non-zero value.
468     * @return A new distribution that in normalised through dividing by the specified value.
469     */
470    public Distribution<T> normaliseBy(double value) {
471            
472            if (0 == value)
473                    throw new IllegalArgumentException("Cannot normalise by 0");
474            
475            TreeMap<T, Integer> normalised = new TreeMap<T, Integer>();
476            for (Entry<T, Integer> entry : dist.entrySet()) {
477                    
478                    int normFreq = (int) Math.round(entry.getValue().intValue() / value);
479                    if (0 < normFreq)
480                            normalised.put(entry.getKey(), normFreq);
481            }
482            
483            return new Distribution<T>(normalised);
484    }
485    
486    /**
487     * Returns a new Distribution in which each observation frequency equals to the logarithm of
488     * the corresponding observation frequency of this distribution;
489     * all resulting non-integer frequencies are rounded to the nearest integer.
490     * 
491     * @param base The base of the logarithm to use in obtainign the new distribution sample.
492     * @return A new log distribution obtained from this distribution.
493     */
494    public Distribution<T> getLogDistribution(double base) {
495            
496            if (Double.isNaN(base) || Double.isInfinite(base))
497                    throw new IllegalArgumentException("Log base must be a real positive number");
498            if (0 >= base)
499                    throw new IllegalArgumentException("Log base must be positive");
500            
501            TreeMap<T, Integer> logDist = new TreeMap<T, Integer>();
502            for (Entry<T, Integer> entry : dist.entrySet()) {
503                    
504                    int logFreq = (int) Math.round(MathTools.log(base, (double) entry.getValue().intValue()));
505                    if (0 < logFreq)
506                            logDist.put(entry.getKey(), logFreq);
507            }
508            
509            return new Distribution<T>(logDist);
510    }
511    
512    /**
513     * Gets the data of this distribution as two arrays - one containing the observations, and
514     * the other containing the corresponding frequencies.
515     * 
516     * @return A {@code Pair} of {@code array}s, where the first element of the pair is an
517     * array contaning all observations in this sample in ascending order and the second
518     * element of the pair is an array containing the respective observation frequencies.
519     */
520    public Pair<T[], Integer[]> getData() {
521            
522            @SuppressWarnings("unchecked")
523            T[] X = (T[]) new Number[dist.size()];
524            
525            Integer[] Y = new Integer[dist.size()];
526    
527            int i = 0;
528            for (Entry<T, Integer> entry : dist.entrySet()) {
529                    X[i] = entry.getKey();
530                    Y[i] = entry.getValue();
531                    i++;
532            }
533            return new Pair<T[], Integer[]>(X, Y);
534    }
535    
536    /**
537     * Computes the standard deviation of this sample.
538     * This method converts the observations contained in this sample to {@code double} values;
539     * be careful as this might have consequences if the type of the values in this sample cannot
540     * be exactly converted to {@code double}.
541     * 
542     * @return The standard deviation of this sample.
543     */
544    public double getStdDeviation() {
545            return Math.sqrt(variance);
546    }
547    
548    
549    /**
550     * Gets the smallest observation in this sample.
551     * 
552     * @return The smallest observation in this sample or {@code null} is this sample is empty.
553     */
554    public T getMin() {
555            return minObservation;
556    }
557    
558    
559    /**
560     * Gets the largest observation in this sample.
561     * 
562     * @return The largest observation in this sample or {@code null} is this sample is empty.
563     */
564    public T getMax() {
565            return maxObservation;
566    }
567    
568    
569    /**
570     * Gets the total number of observations in this sample.
571     * If this sample is very large, it may be prefereable to use {@link #countObservationsL()}.
572     * 
573     * @return The size of this sample.
574     */
575    public int countObservations() {
576            return (int) observationCount;
577    }
578    
579    /**
580     * Gets the total number of observations in this sample.
581     * 
582     * @return The size of this sample.
583     */
584    public long countObservationsL() {
585            return observationCount;
586    }
587    
588    /**
589     * Creates a {@code String} that contains some basic statistical information about this sample,
590     * such as the number of observations, mean, variance and standard deviation.
591     * 
592     * @return A {@code String} that contains some basic statistical information about this sample.
593     */
594    public String getStringStats() {
595            StringBuffer s = new StringBuffer();
596            s.append("OBSERVATIONS: \t");
597            s.append(observationCount);
598            s.append(getNewLine());
599            s.append("MEAN:         \t");
600            s.append(getMean());
601            s.append(getNewLine());
602            s.append("VARIANCE:     \t");
603            s.append(getVariance());
604            s.append(getNewLine());
605            s.append("STD DEVIATION:\t");
606            s.append(getStdDeviation());
607            s.append(getNewLine());
608            return s.toString();
609    }
610    
611    /**
612     * Creates a {@code String} with a distribution table of this sample. The {@code toString()}
613     * method of the sample values is used and not type specific formating is performed.
614     * 
615     * @param extraInfo Whether to append the result of {@link #getStringStats()} to the distribution
616     * table.
617     * @return A distribution table of this sample.
618     */
619    public String getStringTable(boolean extraInfo) {       
620            StringBuffer s = new StringBuffer();    
621            for (Entry<T, Integer> entry : dist.entrySet()) {
622                    s.append(entry.getKey());
623                    s.append(":\t");
624                    s.append(entry.getValue());
625                    s.append("\t (");
626                    s.append(String.format("%08.5f", 100. * entry.getValue().doubleValue() / observationCount));
627                    s.append("%)");
628                    s.append(getNewLine());         
629            }
630            if (extraInfo)
631                    s.append(getStringStats());
632            return s.toString();
633    }
634    
635    /**
636     * This is equivalent to {@code getStringTable(true)}.
637     * 
638     * @return A distribution table of this sample.
639     */
640    public String getStringTable() {        
641            return getStringTable(true);
642    }
643    
644    
645    /**
646     * Creates a {@code String} with a distribution table of this sample. The observation values
647     * are formatted as {@code double}s.
648     * 
649     * @param extraInfo Whether to append the result of {@link #getStringStats()} to the distribution
650     * table.
651     * @return A distribution table of this sample.
652     */
653    public String getStringTableD(boolean extraInfo) {      
654            StringBuffer s = new StringBuffer();    
655            for (Entry<T, Integer> entry : dist.entrySet()) {
656                    int count = entry.getValue();
657                    s.append(String.format("%18.010f: %11d (%08.5f%%)%n",
658                                    entry.getKey().doubleValue(), count,  100.0 * count / observationCount));
659            }
660            if (extraInfo)
661                    s.append(getStringStats());
662            return s.toString();
663    }
664    
665    /**
666     * This is equivalent to {@code getStringTableD(true)}.
667     * 
668     * @return A distribution table of this sample.
669     */
670    public String getStringTableD() {       
671            return getStringTableD(true);
672    }
673    
674    
675    /**
676     * Creates a {@code String} with a distribution table of this sample. The observation values
677     * are formatted as {@code int}s.
678     * 
679     * @param extraInfo Whether to append the result of {@link #getStringStats()} to the distribution
680     * table.
681     * @return A distribution table of this sample.
682     */
683    public String getStringTableI(boolean extraInfo) {      
684            StringBuffer s = new StringBuffer();    
685            for (Entry<T, Integer> entry : dist.entrySet()) {
686                    int count = entry.getValue();
687                    s.append(String.format("%11d: %11d (%08.5f%%)%n",
688                                    entry.getKey().intValue(), count, 100.0 * count / observationCount));
689            }
690            if (extraInfo)
691                    s.append(getStringStats());
692            return s.toString();
693    }
694    
695    
696    /**
697     * This is equivalent to {@code getStringTableI(true)}.
698     * 
699     * @return A distribution table of this sample.
700     */
701    public String getStringTableI() {       
702            return getStringTableI(true);
703    }
704    
705    
706    /**
707     * This method creates a string that - if saved to a file - can be loaded by the
708     * LiveGraph-plotter in order to plot this dirtribution sample.<br />
709     * <br />
710     * The LiveGraph plotter framework is an open-source project written in Java available from
711     * <a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>.<br />
712     * <br />
713     * The string returned by this method encodes all observations of this sample. For a
714     * straight-forward method of saving the string to a file, see
715     * {@link com.softnetConsult.utils.files.FileTools#writeToFile(String, String, boolean)}.
716     * 
717     * @return An encoding of this distribution to be used with the LiveGraph plotter.
718     */
719    public String getStringLiveGraphPlot() {
720            return getStringLiveGraphPlot((String[]) null); 
721    }
722    
723    /**
724     * This method creates a string that - if saved to a file - can be loaded by the
725     * LiveGraph-plotter in order to plot this dirtribution sample.<br />
726     * <br />
727     * The LiveGraph plotter framework is an open-source project written in Java available from
728     * <a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>.<br />
729     * <br />
730     * The string returned by this method encodes all observations of this sample. For a
731     * straight-forward method of saving the string to a file, see
732     * {@link com.softnetConsult.utils.files.FileTools#writeToFile(String, String, boolean)}.
733     * 
734     * @param info An info string to add as data file annotation.
735     * @return An encoding of this distribution to be used with the LiveGraph plotter.
736     */
737    public String getStringLiveGraphPlot(final String info) {
738            return getStringLiveGraphPlot(new String[] {info});
739    }
740    
741    /**
742     * This method creates a string that - if saved to a file - can be loaded by the
743     * LiveGraph-plotter in order to plot this dirtribution sample.<br />
744     * <br />
745     * The LiveGraph plotter framework is an open-source project written in Java available from
746     * <a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>.<br />
747     * <br />
748     * The string returned by this method encodes all observations of this sample. For a
749     * straight-forward method of saving the string to a file, see
750     * {@link com.softnetConsult.utils.files.FileTools#writeToFile(String, String, boolean)}.
751     * 
752     * @param infos An list of info strings to add as data file annotation.
753     * @return An encoding of this distribution to be used with the LiveGraph plotter.
754     */
755    public String getStringLiveGraphPlot(final String[] infos) {
756            StringBuffer s = new StringBuffer();
757            s.append("@Distribution Plot");
758            s.append(getNewLine());
759            s.append("@Generated by com.softnetConsult.utils.math.Distribution");
760            s.append(getNewLine());
761            s.append("@(see http://java-tools.sourceforge.net/)");
762            s.append(getNewLine());
763            if (null != infos && 0 < infos.length) {
764                    s.append("@ ");
765                    s.append(getNewLine());
766                    for (String info : infos) {
767                            s.append("@ ");
768                            s.append(null == info ? "" : info);
769                            s.append(getNewLine());
770                    }
771            }
772            s.append("@ ");
773            s.append(getNewLine());
774            s.append("@Observations: \t");
775            s.append(observationCount);
776            s.append(getNewLine());
777            s.append("@Mean:         \t");
778            s.append(getMean());
779            s.append(getNewLine());
780            s.append("@Variance:     \t");
781            s.append(getVariance());
782            s.append(getNewLine());
783            s.append("@Std Deviation:\t");
784            s.append(getStdDeviation());
785            s.append(getNewLine());
786            s.append("Observation, Frequency, Per Cent");
787            s.append(getNewLine());
788            for (Entry<T, Integer> entry : dist.entrySet()) {
789                    s.append(entry.getKey());
790                    s.append(", ");
791                    s.append(entry.getValue());
792                    s.append(", ");
793                    s.append(100.0 * entry.getValue().doubleValue() / observationCount);
794                    s.append(getNewLine());         
795            }
796            return s.toString();
797    }
798    
799    } // public class Distribution<T extends Number>