Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Scatterplot: Draw separate regression lines for colors; add orthonormal regression #3518

Merged
merged 3 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 84 additions & 23 deletions Orange/widgets/visualize/owscatterplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,24 @@ def score_heuristic(self):

class OWScatterPlotGraph(OWScatterPlotBase):
show_reg_line = Setting(False)
orthonormal_regression = Setting(False)

def __init__(self, scatter_widget, parent):
super().__init__(scatter_widget, parent)
self.reg_line_item = None
self.reg_line_items = []

def clear(self):
super().clear()
self.reg_line_item = None
self.reg_line_items.clear()

def update_coordinates(self):
super().update_coordinates()
self.update_axes()
# Don't update_regression line here: update_coordinates is always
# followed by update_point_props, which calls update_colors

def update_colors(self):
super().update_colors()
self.update_regression_line()

def update_axes(self):
Expand All @@ -117,33 +123,80 @@ def update_axes(self):
if title is None:
self.plot_widget.hideAxis(axis)

def update_regression_line(self):
if self.reg_line_item is not None:
self.plot_widget.removeItem(self.reg_line_item)
self.reg_line_item = None
if not self.show_reg_line:
return
x, y = self.master.get_coordinates_data()
if x is None:
return
@staticmethod
def _orthonormal_line(x, y, color, width):
# https://en.wikipedia.org/wiki/Deming_regression, with δ=0.
pen = pg.mkPen(color=color, width=width)
xm = np.mean(x)
ym = np.mean(y)
sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()

if sxy != 0: # also covers sxx != 0 and syy != 0
slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \
/ (2 * sxy)
intercept = ym - slope * xm
xmin = x.min()
return pg.InfiniteLine(
QPointF(xmin, xmin * slope + intercept),
np.degrees(np.arctan(slope)),
pen)
elif (sxx == 0) == (syy == 0): # both zero or non-zero -> can't draw
return None
elif sxx != 0:
return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)
else:
return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)

@staticmethod
def _regression_line(x, y, color, width):
min_x, max_x = np.min(x), np.max(x)
if min_x == max_x:
return None
slope, intercept, rvalue, _, _ = linregress(x, y)
angle = np.degrees(np.arctan(slope))
start_y = min_x * slope + intercept
end_y = max_x * slope + intercept
angle = np.degrees(np.arctan((end_y - start_y) / (max_x - min_x)))
rotate = ((angle + 45) % 180) - 45 > 90
color = QColor("#505050")
l_opts = dict(color=color, position=abs(int(rotate) - 0.85),
rotate = 135 < angle % 360 < 315
l_opts = dict(color=color, position=abs(rotate - 0.85),
rotateAxis=(1, 0), movable=True)
self.reg_line_item = pg.InfiniteLine(
reg_line_item = pg.InfiniteLine(
pos=QPointF(min_x, start_y), angle=angle,
pen=pg.mkPen(color=color, width=1),
label="r = {:.2f}".format(rvalue), labelOpts=l_opts
)
pen=pg.mkPen(color=color, width=width),
label=f"r = {rvalue:.2f}", labelOpts=l_opts)
if rotate:
self.reg_line_item.label.angle = 180
self.reg_line_item.label.updateTransform()
self.plot_widget.addItem(self.reg_line_item)
reg_line_item.label.angle = 180
reg_line_item.label.updateTransform()
return reg_line_item

def _add_line(self, x, y, color, width):
if self.orthonormal_regression:
line = self._orthonormal_line(x, y, color, width)
else:
line = self._regression_line(x, y, color, width)
if line is None:
return
self.plot_widget.addItem(line)
self.reg_line_items.append(line)

def update_regression_line(self):
for line in self.reg_line_items:
self.plot_widget.removeItem(line)
self.reg_line_items.clear()
if not self.show_reg_line:
return
x, y = self.master.get_coordinates_data()
if x is None:
return
self._add_line(x, y, QColor("#505050"), width=2)
if self.master.is_continuous_color() or self.palette is None:
return
c_data = self.master.get_color_data()
if c_data is None:
return
c_data = c_data.astype(int)
for val in range(c_data.max() + 1):
mask = c_data == val
if mask.sum() > 1:
self._add_line(x[mask], y[mask], self.palette[val], width=2)


class OWScatterPlot(OWDataProjectionWidget):
Expand Down Expand Up @@ -208,6 +261,14 @@ def _add_controls(self):
self.gui.ToolTipShowsAll,
self.gui.RegressionLine],
self._plot_box)
gui.checkBox(
gui.indentedBox(self._plot_box), self,
value="graph.orthonormal_regression",
label="Treat variables as independent",
callback=self.graph.update_regression_line,
tooltip=
"If checked, fit line to group (minimize distance from points);\n"
"otherwise fit y as a function of x (minimize vertical distances)")

def _add_controls_axis(self):
common_options = dict(
Expand Down
Loading