Initial revision
[python_utils.git] / histogram.py
1 #!/usr/bin/env python3
2
3 import math
4 from numbers import Number
5 from typing import Generic, Iterable, List, Optional, Tuple, TypeVar
6
7 from math_utils import RunningMedian
8 from text_utils import bar_graph
9
10
11 T = TypeVar("T", bound=Number)
12
13
14 class SimpleHistogram(Generic[T]):
15
16     # Useful in defining wide open bottom/top bucket bounds:
17     POSITIVE_INFINITY = math.inf
18     NEGATIVE_INFINITY = -math.inf
19
20     def __init__(self, buckets: List[Tuple[T, T]]):
21         self.buckets = {}
22         for start_end in buckets:
23             if self._get_bucket(start_end[0]) is not None:
24                 raise Exception("Buckets overlap?!")
25             self.buckets[start_end] = 0
26         self.sigma = 0
27         self.median = RunningMedian()
28         self.maximum = None
29         self.minimum = None
30         self.count = 0
31
32     @staticmethod
33     def n_evenly_spaced_buckets(
34             min_bound: T,
35             max_bound: T,
36             n: int,
37     ) -> List[Tuple[T, T]]:
38         ret = []
39         stride = int((max_bound - min_bound) / n)
40         if stride <= 0:
41             raise Exception("Min must be < Max")
42         for bucket_start in range(min_bound, max_bound, stride):
43             ret.append((bucket_start, bucket_start + stride))
44         return ret
45
46     def _get_bucket(self, item: T) -> Optional[Tuple[T, T]]:
47         for start_end in self.buckets:
48             if start_end[0] <= item < start_end[1]:
49                 return start_end
50         return None
51
52     def add_item(self, item: T) -> bool:
53         bucket = self._get_bucket(item)
54         if bucket is None:
55             return False
56         self.count += 1
57         self.buckets[bucket] += 1
58         self.sigma += item
59         self.median.add_number(item)
60         if self.maximum is None or item > self.maximum:
61             self.maximum = item
62         if self.minimum is None or item < self.minimum:
63             self.minimum = item
64         return True
65
66     def add_items(self, lst: Iterable[T]) -> bool:
67         all_true = True
68         for item in lst:
69             all_true = all_true and self.add_item(item)
70         return all_true
71
72     def __repr__(self) -> str:
73         max_population: Optional[int] = None
74         for bucket in self.buckets:
75             pop = self.buckets[bucket]
76             if pop > 0:
77                 last_bucket_start = bucket[0]
78             if max_population is None or pop > max_population:
79                 max_population = pop
80         txt = ""
81         if max_population is None:
82             return txt
83
84         for bucket in sorted(self.buckets, key=lambda x : x[0]):
85             pop = self.buckets[bucket]
86             start = bucket[0]
87             end = bucket[1]
88             bar = bar_graph(
89                 (pop / max_population),
90                 include_text = False,
91                 width = 70,
92                 left_end = "",
93                 right_end = "")
94             label = f'{start}..{end}'
95             txt += f'{label:12}: ' + bar + f"({pop}) ({len(bar)})\n"
96             if start == last_bucket_start:
97                 break
98
99         txt = txt + f'''{self.count} item(s)
100 {self.maximum} max
101 {self.minimum} min
102 {self.sigma/self.count:.3f} mean
103 {self.median.get_median()} median'''
104         return txt