summaryrefslogtreecommitdiff
path: root/histogram.py
diff options
context:
space:
mode:
Diffstat (limited to 'histogram.py')
-rw-r--r--histogram.py104
1 files changed, 104 insertions, 0 deletions
diff --git a/histogram.py b/histogram.py
new file mode 100644
index 0000000..ca67839
--- /dev/null
+++ b/histogram.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+
+import math
+from numbers import Number
+from typing import Generic, Iterable, List, Optional, Tuple, TypeVar
+
+from math_utils import RunningMedian
+from text_utils import bar_graph
+
+
+T = TypeVar("T", bound=Number)
+
+
+class SimpleHistogram(Generic[T]):
+
+ # Useful in defining wide open bottom/top bucket bounds:
+ POSITIVE_INFINITY = math.inf
+ NEGATIVE_INFINITY = -math.inf
+
+ def __init__(self, buckets: List[Tuple[T, T]]):
+ self.buckets = {}
+ for start_end in buckets:
+ if self._get_bucket(start_end[0]) is not None:
+ raise Exception("Buckets overlap?!")
+ self.buckets[start_end] = 0
+ self.sigma = 0
+ self.median = RunningMedian()
+ self.maximum = None
+ self.minimum = None
+ self.count = 0
+
+ @staticmethod
+ def n_evenly_spaced_buckets(
+ min_bound: T,
+ max_bound: T,
+ n: int,
+ ) -> List[Tuple[T, T]]:
+ ret = []
+ stride = int((max_bound - min_bound) / n)
+ if stride <= 0:
+ raise Exception("Min must be < Max")
+ for bucket_start in range(min_bound, max_bound, stride):
+ ret.append((bucket_start, bucket_start + stride))
+ return ret
+
+ def _get_bucket(self, item: T) -> Optional[Tuple[T, T]]:
+ for start_end in self.buckets:
+ if start_end[0] <= item < start_end[1]:
+ return start_end
+ return None
+
+ def add_item(self, item: T) -> bool:
+ bucket = self._get_bucket(item)
+ if bucket is None:
+ return False
+ self.count += 1
+ self.buckets[bucket] += 1
+ self.sigma += item
+ self.median.add_number(item)
+ if self.maximum is None or item > self.maximum:
+ self.maximum = item
+ if self.minimum is None or item < self.minimum:
+ self.minimum = item
+ return True
+
+ def add_items(self, lst: Iterable[T]) -> bool:
+ all_true = True
+ for item in lst:
+ all_true = all_true and self.add_item(item)
+ return all_true
+
+ def __repr__(self) -> str:
+ max_population: Optional[int] = None
+ for bucket in self.buckets:
+ pop = self.buckets[bucket]
+ if pop > 0:
+ last_bucket_start = bucket[0]
+ if max_population is None or pop > max_population:
+ max_population = pop
+ txt = ""
+ if max_population is None:
+ return txt
+
+ for bucket in sorted(self.buckets, key=lambda x : x[0]):
+ pop = self.buckets[bucket]
+ start = bucket[0]
+ end = bucket[1]
+ bar = bar_graph(
+ (pop / max_population),
+ include_text = False,
+ width = 70,
+ left_end = "",
+ right_end = "")
+ label = f'{start}..{end}'
+ txt += f'{label:12}: ' + bar + f"({pop}) ({len(bar)})\n"
+ if start == last_bucket_start:
+ break
+
+ txt = txt + f'''{self.count} item(s)
+{self.maximum} max
+{self.minimum} min
+{self.sigma/self.count:.3f} mean
+{self.median.get_median()} median'''
+ return txt