Skip to content

Commit

Permalink
visual pipeline prototype closes #47
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort committed May 21, 2017
1 parent 08cbf0a commit 579f8c9
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 39 deletions.
1 change: 1 addition & 0 deletions docs/api/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ API Reference
yellowbrick.classifier
yellowbrick.cluster
yellowbrick.text
yellowbrick.pipeline
yellowbrick.style
3 changes: 3 additions & 0 deletions tests/test_features/test_pcoords.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def test_integrated_pcoords(self):
# Convert X to an ndarray
X = np.array(X.tolist())

import time
start = time.time()
# Test the visualizer
visualizer = ParallelCoordinates()
visualizer.fit_transform(X, y)
raise Exception("X conversion to {:0.3f} seconds".format(time.time()-start))
86 changes: 60 additions & 26 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,34 @@ class Thing(object):
pass


class EstimatorSpec(BaseEstimator):
class MockEstimator(BaseEstimator):

def fit(self, X, y=None, **kwargs):
return self

MockEstimator = mock.Mock(spec = EstimatorSpec)
class MockVisualEstimator(Visualizer):

def fit(self, X, y=None, **kwargs):
self.draw(**kwargs)
return self

def draw(self, **kwargs):
pass

class TransformerSpec(BaseEstimator, TransformerMixin):

class MockTransformer(BaseEstimator, TransformerMixin):

def fit(self, X, y=None, **kwargs):
return self

def transform(self, X, **kwargs):
return X

MockTransformer = mock.Mock(spec = TransformerSpec)


class VisualTransformerSpec(Visualizer, TransformerMixin):
class MockVisualTransformer(Visualizer, TransformerMixin):

def fit(self, X, y=None, **kwargs):
self.draw(**kwargs)
return self

def transform(self, X, **kwargs):
Expand All @@ -68,11 +74,6 @@ def transform(self, X, **kwargs):
def draw(self, **kwargs):
pass

def poof(self, **kwargs):
pass

MockVisualTransformer = mock.Mock(spec = VisualTransformerSpec)


##########################################################################
## VisualPipeline Tests
Expand Down Expand Up @@ -145,27 +146,64 @@ def test_visual_steps_property(self):

pipeline = VisualPipeline([
('a', MockTransformer()),
('b', VisualTransformerSpec()),
('b', MockVisualTransformer()),
('c', MockTransformer()),
('d', VisualTransformerSpec()),
('d', MockVisualTransformer()),
('e', MockEstimator()),
])

self.assertNotIn('a', pipeline.visual_steps)
self.assertIn('b', pipeline.visual_steps)
self.assertNotIn('c', pipeline.visual_steps)
self.assertIn('d', pipeline.visual_steps)
self.assertNotIn('e', pipeline.visual_steps)

def test_pipeline_poof(self):
"""
Test the poof call against the VisualPipeline
"""

pipeline = VisualPipeline([
('a', mock.MagicMock(MockTransformer())),
('b', mock.MagicMock(MockVisualTransformer())),
('c', mock.MagicMock(MockTransformer())),
('d', mock.MagicMock(MockVisualTransformer())),
('e', mock.MagicMock(MockEstimator()),)
])

pipeline.poof()
pipeline.steps[1][1].poof.assert_called_once_with(outpath=None)
pipeline.steps[3][1].poof.assert_called_once_with(outpath=None)

def test_pipeline_savefig_poof(self):
"""
Test the poof call with an outdir to save all the figures
"""
pipeline = VisualPipeline([
('a', mock.MagicMock(MockTransformer())),
('b', mock.MagicMock(MockVisualTransformer())),
('c', mock.MagicMock(MockTransformer())),
('d', mock.MagicMock(MockVisualTransformer())),
('e', mock.MagicMock(MockVisualEstimator()),)
])

pipeline.poof(outdir="/tmp/figures")
pipeline.steps[1][1].poof.assert_called_once_with(outpath="/tmp/figures/b.pdf")
pipeline.steps[3][1].poof.assert_called_once_with(outpath="/tmp/figures/d.pdf")
pipeline.steps[4][1].poof.assert_called_once_with(outpath="/tmp/figures/e.pdf")

@unittest.skip("mocks don't work for some reason")
@unittest.skip("need to find a way for fit to return self in mocks")
def test_fit_transform_poof_and_draw_calls(self):
"""
Test calling fit, transform, draw and poof on the pipeline
Test calling fit, transform, and poof on the pipeline
"""

pipeline = VisualPipeline([
('a', MockTransformer()),
('b', MockVisualTransformer()),
('c', MockTransformer()),
('d', MockVisualTransformer()),
('e', MockEstimator()),
('a', mock.MagicMock(MockTransformer())),
('b', mock.MagicMock(MockVisualTransformer())),
('c', mock.MagicMock(MockTransformer())),
('d', mock.MagicMock(MockVisualTransformer())),
('e', mock.MagicMock(MockEstimator()),)
])

X = [[1, 1, 1, 1, 1],
Expand All @@ -180,14 +218,10 @@ def test_fit_transform_poof_and_draw_calls(self):

pipeline.transform(X)
for name, step in pipeline.named_steps.items():
if name == 'e': continue
step.transform.assert_called_once_with(X)

pipeline.draw()
for name, step in pipeline.named_steps.items():
if name in {'a', 'c', 'e'}: continue
step.draw.assert_called_once_with()

pipeline.poof()
for name, step in pipeline.named_steps.items():
if name in {'a', 'c', 'e'}: continue
step.poof.assert_called_once_with()
step.poof.assert_called_once_with(outpath=None)
21 changes: 21 additions & 0 deletions tests/test_utils/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# tests.test_utils.test_helpers
# Tests for the stand alone helper functions in Yellowbrick utils.
#
Expand Down Expand Up @@ -135,6 +136,26 @@ def test_has_ndarray_int_columns_false_outside_column_range(self):
self.assertFalse(has_ndarray_int_columns(features, x))


##########################################################################
## String Helpers Tests
##########################################################################

class StringHelpersTests(unittest.TestCase):

def test_slugifiy(self):
"""
Test the slugify helper utility
"""

cases = (
("This is a test ---", "this-is-a-test"),
("This -- is a ## test ---" , "this-is-a-test"),
)

for case, expected in cases:
self.assertEqual(expected, slugify(case))


##########################################################################
## Execute Tests
##########################################################################
Expand Down
39 changes: 26 additions & 13 deletions yellowbrick/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
## Imports
##########################################################################

from os import path
from .base import Visualizer
from .utils.helpers import slugify
from sklearn.pipeline import Pipeline


Expand Down Expand Up @@ -71,25 +73,36 @@ def visual_steps(self):
if isinstance(step[1], Visualizer)
)

def draw(self, *args, **kwargs):
def poof(self, outdir=None, ext=".pdf", **kwargs):
"""
Calls draw on steps (including the final estimator) that has a draw
method and passes the args and kwargs to that draw function.
A single entry point to rendering all visualizations in the visual
pipeline. The rendering for the output depends on the backend context,
but for path based renderings (e.g. saving to a file), specify a
directory and extension to compse an outpath to save each
visualization (file names will be based on the named step).
Parameters
----------
outdir : path
The directory to save visualizations to.
ext : string, default = ".pdf"
The extension of the file to save the visualization to.
kwargs : dict
Keyword arguments to pass to the ``poof()`` method of all steps.
"""
for name, step in self.visual_steps.items():
step.draw(*args, **kwargs)
if outdir is not None:
outpath = path.join(outdir, slugify(name) + ext)
else:
outpath = None

def poof(self, *args, **kwargs):
"""
Calls poof on steps (including the final estimator) that has a poof
method and passes the args and kwargs to that poof function.
"""
for name, step in self.visual_steps.items():
step.poof(*args, **kwargs)
step.poof(outpath=outpath, **kwargs)

def fit_transform_poof(self, X, y=None, **kwargs):
def fit_transform_poof(self, X, y=None, outpath=None, **kwargs):
"""
Fit the model and transforms and then call poof.
"""
self.fit_transform(X, y, **kwargs)
self.poof(**kwargs)
self.poof(outpath, **kwargs)
28 changes: 28 additions & 0 deletions yellowbrick/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
## Imports
##########################################################################

import re
import unicodedata
import numpy as np

from sklearn.pipeline import Pipeline
Expand Down Expand Up @@ -103,3 +105,29 @@ def div_safe( numerator, denominator ):
return result
except ValueError as e:
raise e


##########################################################################
## String Computations
##########################################################################

def slugify(text):
"""
Returns a slug of given text, normalizing unicode data for file-safe
strings. Used for deciding where to write images to disk.
Parameters
----------
text : string
The string to slugify
Returns
-------
slug : string
A normalized slug representation of the text
.. seealso:: http://yashchandra.com/2014/05/08/how-to-generate-clean-url-or-a-slug-in-python/
"""
slug = re.sub(r'[^\w]+', ' ', text)
slug = "-".join(slug.lower().strip().split())
return slug

0 comments on commit 579f8c9

Please sign in to comment.