Added BoxPlot

Added ability to plot a box plot.
Added demo of box plot.
Added connecting line to _draw2Objects in demo.

TODO:
  + customization
  + refactoring
  + documentation
  + other (see TODO in BoxPlot.draw method)
This commit is contained in:
Douglas Thor
2015-08-08 10:09:57 -07:00
parent 7851def54d
commit b69feea0d4

View File

@@ -134,6 +134,7 @@ import sys
import wx
import functools
import warnings
from collections import namedtuple
from warnings import warn as _warn
# Needs NumPy
@@ -577,7 +578,7 @@ class PolyMarker(PolyPoints):
return (s, s)
def _drawmarkers(self, dc, coords, marker, size=1):
f = eval('self._' + marker)
f = eval('self._' + marker) # XXX: look into changing this
f(dc, coords, size)
def _circle(self, dc, coords, size=1):
@@ -626,6 +627,242 @@ class PolyMarker(PolyPoints):
dc.DrawLineList(lines.astype(np.int32))
class BoxPlot(PolyPoints):
"""
Class to contain box plots
This class takes care of calculating the box plots.
"""
_attributes = {'colour': 'black',
'width': 1,
'style': wx.PENSTYLE_SOLID,
'legend': '',
'drawstyle': 'line',
}
def __init__(self, points, **attr):
"""
:param data: 1D data to plot.
"""
points = np.array([(i, x) for i, x in enumerate(points)])
self._points = np.array(points).astype(np.float64)
self._logscale = (False, False)
self._pointSize = (1.0, 1.0)
self.currentScale = (1, 1)
self.currentShift = (0, 0)
self.scaled = self.points
self.attributes = {}
self.attributes.update(self._attributes)
for name, value in attr.items():
if name not in self._attributes.keys():
err_txt = "Style attribute incorrect. Should be one of {}"
raise KeyError(err_txt.format(self._attributes.keys()))
self.attributes[name] = value
# PolyPoints.__init__(self, points, attr)
self._bpdata = self.calcBpData()
def boundingBox(self):
"""
Returns bounding box for the plot.
Override method.
"""
minXY = np.array([.75, self._bpdata.min * 0.95])
maxXY = np.array([1.25, self._bpdata.max * 1.05])
return minXY, maxXY
def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
"""override method"""
# return
if len(self.points) == 0:
# no curves to draw
return
# TODO: should this be '!=' rather than 'is not'?
if (scale is not self.currentScale) or (shift is not self.currentShift):
# update point scaling
self.scaled = scale * self.points + shift
self.currentScale = scale
self.currentShift = shift
# else unchanged use the current scaling
def calcBpData(self, data=None):
"""
Box plot points:
Median (50%)
75%
25%
low_whisker = lowest value that's >= (25% - (IQR * 1.5))
high_whisker = highest value that's <= 75% + (IQR * 1.5)
outliers are outside of 1.5 * IQR
Parameters:
-----------
data : array-like
The data to plot
Returns:
--------
bpdata : collections.namedtuple
Descriptive statistics for data:
(min_data, low_whisker, q25, median, q75, high_whisker, max_data)
Notes:
------
# TODO: Verify how NaN is handled then decide how I want to handle it.
"""
if data is None:
data = self.points[:, 1]
min_data = float(np.min(data))
max_data = float(np.max(data))
q25 = float(np.percentile(data, 25))
q75 = float(np.percentile(data, 75))
iqr = q75 - q25
low_whisker = float(data[data >= q25 - 1.5 * iqr].min())
high_whisker = float(data[data <= q75 + 1.5 * iqr].max())
median = float(np.median(data))
BPData = namedtuple("bpdata", ("min", "low_whisker", "q25", "median",
"q75", "high_whisker", "max"))
bpdata = BPData(min_data, low_whisker, q25, median,
q75, high_whisker, max_data)
print(bpdata)
return bpdata
def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)):
"""same as override method, but retuns a value."""
scaled = scale * data + shift
return scaled
def draw(self, dc, printerScale, coord=None):
"""
Draws a box plot on the DC.
TODO:
-----
+ Separate out each draw into individual methods
+ Fix the median line extending outside of the box on the right
+ Allow for multiple box plots side-by-side
+ change the X axis to some labels.
+ Add getLegend method
+ Add getClosestPoint method - links to box plot or outliers
+ Add customization
- Pens and Fills for elements
- outlier sizes
- box width
"""
xpos = 1
box_w = 0.5
# Wisker line ######################################################
# We draw it as one line and then hide the middle part with
# the IQR rectangle
whisker_line = np.array([[xpos, self._bpdata.low_whisker],
[xpos, self._bpdata.high_whisker]])
whisker_line = self._scaleAndShift(whisker_line,
self.currentScale,
self.currentShift)
whisker_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_DOT)
dc.SetPen(whisker_pen)
dc.DrawLines(whisker_line)
# IQR Rectangle ####################################################
iqr_box = [[xpos - box_w / 2, self._bpdata.q75], # top, left
[xpos + box_w / 2, self._bpdata.q25]] # W, H
# Scale it to the plot area
iqr_box = self._scaleAndShift(iqr_box,
self.currentScale,
self.currentShift)
# rectangles are drawn (left, top, width, heigh) so adjust
iqr_box = [iqr_box[0][0], # X (left)
iqr_box[0][1], # Y (top)
iqr_box[1][0] - iqr_box[0][0], # Width
iqr_box[1][1] - iqr_box[0][1]] # Height
box_pen = wx.Pen(wx.BLACK, 3, wx.PENSTYLE_SOLID)
box_brush = wx.Brush(wx.GREEN, wx.BRUSHSTYLE_SOLID)
dc.SetPen(box_pen)
dc.SetBrush(box_brush)
dc.DrawRectangleList([iqr_box])
# Median Line ######################################################
# Last so that it's on top of things.
median_line = np.array([[xpos - box_w / 2, self._bpdata.median],
[xpos + box_w / 2, self._bpdata.median]])
median_line = self._scaleAndShift(median_line,
self.currentScale,
self.currentShift)
median_pen = wx.Pen(wx.BLACK, 4, wx.PENSTYLE_SOLID)
dc.SetPen(median_pen)
dc.DrawLines(median_line)
# Whisker Fence ####################################################
fence_top = np.array([[xpos - box_w * 0.2, self._bpdata.high_whisker],
[xpos + box_w * 0.2, self._bpdata.high_whisker]])
fence_top = self._scaleAndShift(fence_top,
self.currentScale,
self.currentShift)
fence_bottom = np.array([[xpos - box_w * 0.2, self._bpdata.low_whisker],
[xpos + box_w * 0.2, self._bpdata.low_whisker]])
fence_bottom = self._scaleAndShift(fence_bottom,
self.currentScale,
self.currentShift)
fence_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID)
dc.SetPen(fence_pen)
dc.DrawLines(fence_top)
dc.DrawLines(fence_bottom)
# Outlier Points ###################################################
# First figure out which points are outliers
outliers = self.points[:, 1]
outlier_bool = np.logical_or(outliers > self._bpdata.high_whisker,
outliers < self._bpdata.low_whisker)
outliers = outliers[outlier_bool]
# Then create a jitter.
jitter = 0.05 * np.random.random_sample(len(outliers)) + 1 - 0.025
pt_data = np.array([jitter, outliers]).T
pt_data = self._scaleAndShift(pt_data,
self.currentScale,
self.currentShift)
outlier_pen = wx.Pen(wx.BLUE, 5, wx.PENSTYLE_SOLID)
dc.SetPen(outlier_pen)
size = 0.5
fact = 2.5 * size
wh = 5.0 * size
rect = np.zeros((len(pt_data), 4), np.float) + [0.0, 0.0, wh, wh]
rect[:, 0:2] = pt_data - [fact, fact]
dc.DrawRectangleList(rect.astype(np.int32))
class PlotGraphics(object):
"""
Container to hold PolyXXX objects and graph labels
@@ -3072,8 +3309,13 @@ def _draw2Objects():
fillstyle=wx.CROSSDIAG_HATCH,
marker='square',
)
marker_line = PolyLine(pts,
legend='Cross Hatch Square',
colour='blue',
width=3,
)
return PlotGraphics([markers1, line1, line2],
return PlotGraphics([markers1, line1, line2, marker_line],
"Big Markers with Different Line Styles")
@@ -3149,6 +3391,22 @@ def _draw7Objects():
"Value Y")
def _draw8Objects():
"""
Box plot
"""
data = np.array([912, 337, 607, 583, 512, 531, 558, 381, 621, 574,
538, 577, 679, 415, 454, 417, 635, 319, 350, 183,
863, 337, 607, 583, 512, 531, 558, 381, 621, 574,
538, 577, 679, 415, 454, 417, 635, 319, 350, 97])
boxplot = BoxPlot(data)
return PlotGraphics([boxplot],
"Box Plot",
"",
"Value")
class TestFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title,
@@ -3177,7 +3435,7 @@ class TestFrame(wx.Frame):
menu = wx.Menu()
menu.Append(206, 'Draw1 - sin, cos', 'Draw plots1')
self.Bind(wx.EVT_MENU, self.OnPlotDraw1, id=206)
menu.Append(207, 'Draw2 - sin, cos, large markers', 'Draw plots2')
menu.Append(207, 'Draw2 - sin, cos, large joined markers', 'Draw plots2')
self.Bind(wx.EVT_MENU, self.OnPlotDraw2, id=207)
menu.Append(208, 'Draw3 - various markers', 'Draw plots3')
self.Bind(wx.EVT_MENU, self.OnPlotDraw3, id=208)
@@ -3189,6 +3447,8 @@ class TestFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.OnPlotDraw6, id=260)
menu.Append(261, 'Draw7 - log-log', 'Draw plots7')
self.Bind(wx.EVT_MENU, self.OnPlotDraw7, id=261)
menu.Append(262, 'Draw8 - Box Plots', 'Draw plots8')
self.Bind(wx.EVT_MENU, self.OnPlotDraw8, id=262)
menu.Append(211, '&Redraw', 'Redraw plots')
self.Bind(wx.EVT_MENU, self.OnPlotRedraw, id=211)
@@ -3420,6 +3680,12 @@ class TestFrame(wx.Frame):
self.client.LogScale = (True, True)
self.client.Draw(_draw7Objects())
def OnPlotDraw8(self, event):
"""Box Plot example"""
self.resetDefaults()
# self.client.EnableAxesValues([False, True])
self.client.Draw(_draw8Objects())
def OnPlotRedraw(self, event):
self.client.Redraw()