diff --git a/composeml/conftest.py b/composeml/conftest.py index 7339d36c..d4d72014 100644 --- a/composeml/conftest.py +++ b/composeml/conftest.py @@ -1,7 +1,30 @@ import pandas as pd import pytest -from .label_times import LabelTimes +from composeml import LabelTimes +from composeml.tests.utils import read_csv + + +@pytest.fixture(scope="module") +def total_spent(): + data = [ + 'id,customer_id,cutoff_time,total_spent', + '0,0,2019-01-01 08:00:00,9', + '1,0,2019-01-01 08:30:00,8', + '2,1,2019-01-01 09:00:00,7', + '3,1,2019-01-01 09:30:00,6', + '4,1,2019-01-01 10:00:00,5', + '5,2,2019-01-01 10:30:00,4', + '6,2,2019-01-01 11:00:00,3', + '7,2,2019-01-01 11:30:00,2', + '8,2,2019-01-01 12:00:00,1', + '9,3,2019-01-01 12:30:00,0', + ] + + data = read_csv(data, index_col='id', parse_dates=['cutoff_time']) + lt = LabelTimes(data=data, name='total_spent') + lt.settings.update({'num_examples_per_instance': -1}) + return lt @pytest.fixture(scope="module") diff --git a/composeml/label_maker.py b/composeml/label_maker.py index 29746029..92495ca2 100644 --- a/composeml/label_maker.py +++ b/composeml/label_maker.py @@ -138,7 +138,7 @@ def __str__(self): class LabelMaker: """Automatically makes labels for prediction problems.""" - def __init__(self, target_entity, time_index, labeling_function, window_size=None): + def __init__(self, target_entity, time_index, labeling_function, window_size=None, label_type=None): """Creates an instance of label maker. Args: @@ -285,6 +285,7 @@ def search(self, minimum_data=None, gap=None, drop_empty=True, + label_type=None, verbose=True, *args, **kwargs): @@ -297,6 +298,7 @@ def search(self, gap (str or int) : Time between examples. Default value is window size. If an integer, search will start on the first event after the minimum data. drop_empty (bool) : Whether to drop empty slices. Default value is True. + label_type (str) : The label type can be "continuous" or "categorical". Default value is the inferred label type. verbose (bool) : Whether to render progress bar. Default value is True. *args : Positional arguments for labeling function. **kwargs : Keyword arguments for labeling function. @@ -353,16 +355,19 @@ def search(self, progress_bar.update(n=total) progress_bar.close() - labels = LabelTimes(data=labels, name=name, target_entity=self.target_entity) + labels = LabelTimes(data=labels, name=name, target_entity=self.target_entity, label_type=label_type) labels = labels.rename_axis('id', axis=0) - labels = labels._with_plots() if labels.empty: return labels + if labels.is_discrete: + labels[labels.name] = labels[labels.name].astype('category') + labels.settings.update({ + 'labeling_function': name, 'num_examples_per_instance': num_examples_per_instance, - 'minimum_data': minimum_data, + 'minimum_data': str(minimum_data), 'window_size': self.window_size, 'gap': gap, }) diff --git a/composeml/label_plots.py b/composeml/label_plots.py new file mode 100644 index 00000000..efd523b9 --- /dev/null +++ b/composeml/label_plots.py @@ -0,0 +1,88 @@ +import matplotlib as mpl +import pandas as pd +import seaborn as sns + +pd.plotting.register_matplotlib_converters() +sns.set_context('notebook') +sns.set_style('darkgrid') +COLOR = sns.color_palette("Set1", n_colors=100, desat=.75) + + +class LabelPlots: + """Creates plots for Label Times.""" + + def __init__(self, label_times): + """Initializes Label Plots. + + Args: + label_times (LabelTimes) : instance of Label Times + """ + self._label_times = label_times + + def count_by_time(self, ax=None, **kwargs): + """Plots the label distribution across cutoff times.""" + count_by_time = self._label_times.count_by_time + count_by_time.sort_index(inplace=True) + + ax = ax or mpl.pyplot.axes() + vmin = count_by_time.index.min() + vmax = count_by_time.index.max() + ax.set_xlim(vmin, vmax) + + locator = mpl.dates.AutoDateLocator() + formatter = mpl.dates.AutoDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + ax.figure.autofmt_xdate() + + if len(count_by_time.shape) > 1: + ax.stackplot( + count_by_time.index, + count_by_time.values.T, + labels=count_by_time.columns, + colors=COLOR, + alpha=.9, + **kwargs, + ) + + ax.legend( + loc='upper left', + title=self._label_times.name, + facecolor='w', + framealpha=.9, + ) + + ax.set_title('Label Count vs. Cutoff Times') + ax.set_ylabel('Count') + ax.set_xlabel('Time') + + else: + ax.fill_between( + count_by_time.index, + count_by_time.values.T, + color=COLOR[1], + ) + + ax.set_title('Label vs. Cutoff Times') + ax.set_ylabel(self._label_times.name) + ax.set_xlabel('Time') + + return ax + + @property + def dist(self): + """Alias for distribution.""" + return self.distribution + + def distribution(self, **kwargs): + """Plots the label distribution.""" + dist = self._label_times[self._label_times.name] + + if self._label_times.is_discrete: + ax = sns.countplot(dist, palette=COLOR, **kwargs) + else: + ax = sns.distplot(dist, kde=True, color=COLOR[1], **kwargs) + + ax.set_title('Label Distribution') + ax.set_ylabel('Count') + return ax diff --git a/composeml/label_times.py b/composeml/label_times.py index cdbf4267..96243fca 100644 --- a/composeml/label_times.py +++ b/composeml/label_times.py @@ -1,73 +1,87 @@ import pandas as pd +from composeml.label_plots import LabelPlots + class LabelTimes(pd.DataFrame): - """ - A data frame containing labels made by a label maker. + """A data frame containing labels made by a label maker. Attributes: name target_entity transforms """ - _metadata = ['name', 'target_entity', 'settings', 'transforms'] - - def __init__(self, data=None, name=None, target_entity=None, settings=None, transforms=None, *args, **kwargs): + _metadata = ['name', 'target_entity', 'settings', 'transforms', 'label_type'] + + def __init__(self, + data=None, + name=None, + target_entity=None, + settings=None, + transforms=None, + label_type=None, + *args, + **kwargs): super().__init__(data=data, *args, **kwargs) self.name = name self.target_entity = target_entity - self.settings = settings or {} self.transforms = transforms or [] + self.plot = LabelPlots(self) + + if label_type is not None: + error = 'label type must be "continuous" or "discrete"' + assert label_type in ['continuous', 'discrete'], error + + self.label_type = label_type + self.settings = settings or {} + self.settings['label_type'] = self.label_type @property def _constructor(self): return LabelTimes @property - def distribution(self): - labels = self.assign(count=1) - labels = labels.groupby(self.name) - distribution = labels['count'].count() - return distribution + def is_discrete(self): + """Whether labels are discrete.""" + if self.label_type is None: + self.label_type = self.infer_type() + self.settings['label_type'] = self.label_type - def _plot_distribution(self, **kwargs): - plot = self.distribution.plot(kind='bar', **kwargs) - plot.set_title('Label Distribution') - plot.set_ylabel('count') - return plot + return self.label_type == 'discrete' + + @property + def distribution(self): + """Returns label distribution if labels are discrete.""" + if self.is_discrete: + labels = self.assign(count=1) + labels = labels.groupby(self.name) + distribution = labels['count'].count() + return distribution @property def count_by_time(self): - count = self.assign(count=1) - count = count.sort_values('cutoff_time') - count = count.set_index([self.name, 'cutoff_time']) - count = count.groupby(self.name) - count = count['count'].cumsum() - return count - - def _plot_count_by_time(self, **kwargs): - count = self.count_by_time - count = count.unstack(self.name) - count = count.ffill() - - plot = count.plot(kind='area', **kwargs) - plot.set_title('Label Count vs. Time') - plot.set_ylabel('count') - return plot - - def _with_plots(self): - self.plot.count_by_time = self._plot_count_by_time - self.plot.distribution = self._plot_distribution - return self + """Returns label count across cutoff times.""" + if self.is_discrete: + keys = ['cutoff_time', self.name] + value = self.groupby(keys).cutoff_time.count() + value = value.unstack(self.name).fillna(0) + value = value.cumsum() + return value + else: + value = self.groupby('cutoff_time') + value = value[self.name].count() + value = value.cumsum() + return value def describe(self): """Prints out label info with transform settings that reproduce labels.""" - print('Label Distribution\n' + '-' * 18, end='\n') - distribution = self[self.name].value_counts() - distribution.index = distribution.index.astype('str') - distribution['Total:'] = distribution.sum() - print(distribution.to_string(), end='\n\n\n') + if self.is_discrete: + print('Label Distribution\n' + '-' * 18, end='\n') + distribution = self[self.name].value_counts() + distribution.index = distribution.index.astype('str') + distribution['Total:'] = distribution.sum() + print(distribution.to_string(), end='\n\n\n') print('Settings\n' + '-' * 8, end='\n') settings = pd.Series(self.settings) @@ -99,7 +113,7 @@ def copy(self): """ labels = super().copy() labels.transforms = labels.transforms.copy() - return labels._with_plots() + return labels def threshold(self, value, inplace=False): """ @@ -115,6 +129,9 @@ def threshold(self, value, inplace=False): labels = self if inplace else self.copy() labels[self.name] = labels[self.name].gt(value) + labels.label_type = 'discrete' + labels.settings['label_type'] = 'discrete' + transform = {'__name__': 'threshold', 'value': value} labels.transforms.append(transform) @@ -225,6 +242,8 @@ def bin(self, bins, quantiles=False, labels=None, right=True): } label_times.transforms.append(transform) + label_times.label_type = 'discrete' + label_times.settings['label_type'] = 'discrete' return label_times def sample(self, n=None, frac=None, random_state=None): @@ -318,3 +337,19 @@ def sample(self, n=None, frac=None, random_state=None): labels = pd.concat(sample_per_label, axis=0, sort=False) return labels + + def infer_type(self): + """Infer label type. + + Returns: + str : Inferred label type. Either "continuous" or "discrete". + """ + dtype = self[self.name].dtype + is_discrete = pd.api.types.is_bool_dtype(dtype) + is_discrete = is_discrete or pd.api.types.is_categorical_dtype(dtype) + is_discrete = is_discrete or pd.api.types.is_object_dtype(dtype) + + if is_discrete: + return 'discrete' + else: + return 'continuous' diff --git a/composeml/tests/test_label_maker.py b/composeml/tests/test_label_maker.py index cc347332..33ac798c 100644 --- a/composeml/tests/test_label_maker.py +++ b/composeml/tests/test_label_maker.py @@ -421,3 +421,9 @@ def test_slice_overlap(transactions): start, end = df.context.window is_overlap = df.index == end assert not is_overlap.any() + + +def test_label_type(transactions): + lm = LabelMaker(target_entity='customer_id', time_index='time', labeling_function=total_spent) + lt = lm.search(transactions, num_examples_per_instance=1, label_type='discrete', verbose=False) + assert lt.label_type == 'discrete' diff --git a/composeml/tests/test_label_plots.py b/composeml/tests/test_label_plots.py index ad4d8f87..b77629fc 100644 --- a/composeml/tests/test_label_plots.py +++ b/composeml/tests/test_label_plots.py @@ -1,10 +1,21 @@ -def test_distribution_plot(labels): - labels = labels.threshold(200) - plot = labels.plot.distribution() - assert plot.get_title() == 'Label Distribution' +def test_count_by_time_categorical(total_spent): + labels = range(2) + total_spent = total_spent.bin(2, labels=labels) + ax = total_spent.plot.count_by_time() + assert ax.get_title() == 'Label Count vs. Cutoff Times' -def test_count_by_time_plot(labels): - labels = labels.threshold(200) - plot = labels.plot.count_by_time() - assert plot.get_title() == 'Label Count vs. Time' +def test_count_by_time_continuous(total_spent): + ax = total_spent.plot.count_by_time() + assert ax.get_title() == 'Label vs. Cutoff Times' + + +def test_distribution_categorical(total_spent): + ax = total_spent.bin(2, labels=range(2)) + ax = ax.plot.dist() + assert ax.get_title() == 'Label Distribution' + + +def test_distribution_continuous(total_spent): + ax = total_spent.plot.dist() + assert ax.get_title() == 'Label Distribution' diff --git a/composeml/tests/test_label_times.py b/composeml/tests/test_label_times.py index b1305a5c..97b10fba 100644 --- a/composeml/tests/test_label_times.py +++ b/composeml/tests/test_label_times.py @@ -1,9 +1,77 @@ -def test_describe(labels): - labels = labels.bin(2) - labels.settings.update(num_examples_per_instance=2) - assert labels.describe() is None +def test_count_by_time_categorical(total_spent): + labels = range(2) + given_answer = total_spent.bin(2, labels=labels).count_by_time + given_answer = given_answer.to_csv(header=True).splitlines() + answer = [ + 'cutoff_time,0,1', + '2019-01-01 08:00:00,0.0,1.0', + '2019-01-01 08:30:00,0.0,2.0', + '2019-01-01 09:00:00,0.0,3.0', + '2019-01-01 09:30:00,0.0,4.0', + '2019-01-01 10:00:00,0.0,5.0', + '2019-01-01 10:30:00,1.0,5.0', + '2019-01-01 11:00:00,2.0,5.0', + '2019-01-01 11:30:00,3.0,5.0', + '2019-01-01 12:00:00,4.0,5.0', + '2019-01-01 12:30:00,5.0,5.0', + ] -def test_describe_empty(labels): - labels.settings.clear() - assert labels.describe() is None + assert given_answer == answer + + +def test_count_by_time_continuous(total_spent): + given_answer = total_spent.count_by_time + given_answer = given_answer.to_csv(header=True).splitlines() + + answer = [ + 'cutoff_time,total_spent', + '2019-01-01 08:00:00,1', + '2019-01-01 08:30:00,2', + '2019-01-01 09:00:00,3', + '2019-01-01 09:30:00,4', + '2019-01-01 10:00:00,5', + '2019-01-01 10:30:00,6', + '2019-01-01 11:00:00,7', + '2019-01-01 11:30:00,8', + '2019-01-01 12:00:00,9', + '2019-01-01 12:30:00,10', + ] + + assert given_answer == answer + + +def test_describe(total_spent): + assert total_spent.bin(2).describe() is None + + +def test_describe_no_settings(total_spent): + total_spent = total_spent.copy() + total_spent.settings.clear() + assert total_spent.describe() is None + + +def test_distribution_categorical(total_spent): + labels = range(2) + given_answer = total_spent.bin(2, labels=labels).distribution + given_answer = given_answer.to_csv(header=True).splitlines() + + answer = [ + 'total_spent,count', + '0,5', + '1,5', + ] + + assert given_answer == answer + + +def test_distribution_continous(total_spent): + assert total_spent.distribution is None + + +def test_infer_type(total_spent): + assert total_spent.infer_type() == 'continuous' + + total_spent = total_spent.threshold(5) + total_spent.label_type = None + assert total_spent.infer_type() == 'discrete' diff --git a/composeml/tests/test_label_transforms/test_threshold.py b/composeml/tests/test_label_transforms/test_threshold.py index 36a6c937..cffbe49e 100644 --- a/composeml/tests/test_label_transforms/test_threshold.py +++ b/composeml/tests/test_label_transforms/test_threshold.py @@ -1,13 +1,10 @@ -import pandas as pd - - def test_threshold(labels): - given_labels = labels.threshold(200) - transform = given_labels.transforms[0] + labels = labels.threshold(200) + transform = labels.transforms[0] assert transform['__name__'] == 'threshold' assert transform['value'] == 200 answer = [True, False, True, False] - labels = labels.assign(my_labeling_function=answer) - pd.testing.assert_frame_equal(given_labels, labels) + given_answer = labels[labels.name].values.tolist() + assert given_answer == answer diff --git a/composeml/tests/utils.py b/composeml/tests/utils.py index ddc5bb2f..da8cdea1 100644 --- a/composeml/tests/utils.py +++ b/composeml/tests/utils.py @@ -3,11 +3,11 @@ import pandas as pd -def read_csv(csv): +def read_csv(csv, **kwargs): if isinstance(csv, list): csv = '\n'.join(csv) with StringIO(csv) as file: - df = pd.read_csv(file) + df = pd.read_csv(file, **kwargs) return df diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst index 460c4bcd..69b1c966 100644 --- a/docs/source/api_reference.rst +++ b/docs/source/api_reference.rst @@ -35,14 +35,23 @@ Transform Methods LabelTimes.sample LabelTimes.threshold +.. currentmodule:: composeml.label_plots + +Label Plots +=========== + +.. autosummary:: + :toctree: generated + :template: class.rst + :nosignatures: + + LabelPlots + Plotting Methods ---------------- -.. list-table:: - :widths: 25 75 - :header-rows: 0 +.. autosummary:: + :nosignatures: - * - :mod:`LabelTimes.plot.distribution` - - Plot the label distribution. - * - :mod:`LabelTimes.plot.count_by_time` - - Plot the label count vs. time. + LabelPlots.count_by_time + LabelPlots.distribution diff --git a/docs/source/examples/predict-next-purchase/example.ipynb b/docs/source/examples/predict-next-purchase/example.ipynb index 1157c15c..678e6bfe 100644 --- a/docs/source/examples/predict-next-purchase/example.ipynb +++ b/docs/source/examples/predict-next-purchase/example.ipynb @@ -234,7 +234,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Elapsed: 01:30 | Remaining: 00:00 | Progress: 100%|██████████| user_id: 19477/19477 \n" + "Elapsed: 01:37 | Remaining: 00:00 | Progress: 100%|██████████| user_id: 19477/19477 \n" ] }, { @@ -374,6 +374,73 @@ "source": [ "lt.describe()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot Labels\n", + "\n", + "Additionally, there are plots available for insight to the labels.\n", + "\n", + "\n", + "#### Distribution\n", + "\n", + "This plot shows the label distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZMAAAEXCAYAAABoPamvAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAdLUlEQVR4nO3de5RdZZnn8W9VCBJJAhgKCSICg3lAECM3LzSINjbSeFdIQ+SiArIw4oyKlzZB2oWtoz0qKFEniEFjIz1xUFsI2GorQRowMiAXebRnuHQgNDGCSZRAkqr5Y78Fh0oqOVW7zjlU1fezVpZnP/vdZ7+bdTy/et+9z95dfX19SJJUR3enOyBJGv0ME0lSbYaJJKk2w0SSVJthIkmqzTCRJNW2Tac7ILVKROwJ3JGZk4e4XR/Qk5m/H8I2C8u+/mFA/XzgvcADpTQR+DVwXmb+rrS5FTgqMx8d5L13AK7MzNcMsv5W4CjgzcDbM/P1zfa7bH8ecFtmfj8iPgn8e2Z+cyjvIRkmUutdkZlz+hci4mTgpxGxf2auzsyZW9l+J+CwwVb2bx8Rw+3fa4C7ynudN9w30fhmmGhciogZwMXAFGA6cCswKzPXlSafiohDqaaC52bmD8t27wbOLvVVwJzMvHso+87Mb5VAOQn4av9IiOr/j98Edi5Nr8rMecA3gEllBHIw8Gfg+8BLgNnAL8v2ANMj4hpgN+A+4IzMfCgifgZ8OTMXl+P4GfBl4LnAIcDnImIj8CbKCCsijgA+BzwbeKL8d7gmIk4D3gL0Ai8s/Tk1M38zlP8OGls8Z6Lx6gzgssx8ObAPsBdwXMP6/5eZBwHvAC6LiJ6IeBVwKnBEZr4U+Cxw5TD3fxvw4s30qX+/RwAvLFNc7wQey8yZmbkR2Bb458yMzFw24D1mUAXcgcDtwIVb6kRmXgwsA87NzCePJSKmAYuB95f3OhVYFBF7lSavAt6XmQcANwEfHeLxa4wxTDRefQRYGREfBr5C9Zd847mVrwJk5h1UU0CvoAqbfYAbyijhs8BOEfGcYey/j+ov+kbXAG+LiKuB9wAfzcw/DrL90kHqP87Mfy+vvw68dhh9A3gZ1bmTmwAy807gF1TnZgB+lZnLy+tbgOH8N9AYYphovLocOJNqKugLVF+IXQ3rNza87gbWAxOAb5URwkzgIKopokeGsf9DqUYOT8rMX1KNkP4nsCdwc0QcPMj2awepb67fUIVX4/Ftu5X+TSjbNOqmuoAA4LGG+sD31jhkmGi8Ogb4ZGZeUZZfRvUF2u80gIg4iGo0chNwLXBiREwvbc4CfjLUHZfzLnsD/zSg/hlgXmZ+D3g/cCdwALABmBARzXxhvzoi9mjo35LyeiVV8BERLwIObNhmA0+FRL9/A/aNiMPKNvsDRwI/a6IPGoc8Aa+xbvuIGPhX/CuAvwWujIg/AX8Efk4VGv32joj/Q/VX999k5h+AH0XEfwf+JSJ6gdXAWzOzbytXUs2KiL8o79UNJNWlwOsGtPsi1fmZO4DHqc6rfIfqy/5m4M5yUnxLfg1cGhG7Ar+hmi4DuKC893HA3cB1Ddv8APh0RDw5WsnM30fE8cCXIuLZVCfb35mZv42IV26lDxqHurwFvSSpLqe5JEm1GSaSpNoME0lSbYaJJKm28Xg117OorvFfwdOvyZckDW4C1a2Hfkl1teHTjMcwOZTBfz0sSdqyI4DrBxbHY5isAHjkkT/R2+tl0ZLUjO7uLnbaaXso36EDjccw2QjQ29tnmEjS0G329EBLwyQipgI3AK/PzHsb6nOoHuJzVFneA1gE7EL16+DZmbk2InYEvk1164mVwAnldtrbUt3E7hCqewSdNNTbgEuSRk7LruaKiJdRzavNGFB/EZverno+MD8z96W6Hfa8Ur8AWJqZ+wELeOp22ucAfyr1/wosbMUxSJKa08pLg8+gelzpg/2FiHgW8DXgvIbaRKobyC0upYXA8eX1cVQjE6ju8npsaf9kPTOvA3oabm4nSWqzlk1zZebpsMmjRD8NXArc01DbGVidmRvK8gpg9/J6t7JMZm6IiNVUT5R7sj5gm/ub7d+0aUN6LLgkaQvadgI+Il4L7JGZH4iIoxpWdbPpcxN6y/8OvOV2V1k3cJuuhm2asmrVWk/AS1KTuru7tvhHeDt/AX8isH95Qt0lwCERcQXwMLBDRPQ/S2I6T02NPQDsChAR21A9r3sVsLy067drwzaSpDZrW5hk5rsyc7/yhLrTgWWZOSsz11P9iHBWaXoKTz3Q5+qyTFm/tLR/sl6eE7EuM5ue4pIkjaxnyu9MzqZ6cM9cqvMeJ5b6PGBhRNwJPArMLvUvAV8r9ceBk9vcX3acsi0Tt3tWu3erZ7j16x7n0TVPdLobUtuNx4dj7QncU/ecSU/PFK495nUj1imNDcdcew0rV67pdDekEddwzmQv4N5N1re7Q5KksccwkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTatmn1DiJiKnAD8PrMvDcizgTOAfqAZcB7MvOJiJgJXAJMBa4DzsrMDRGxB7AI2AVIYHZmro2IHYFvA3sDK4ETMvOhVh+PJGlTLR2ZRMTLgOuBGWV5BnAu8ErgwLL/95bmi4A5mTkD6ALOKPX5wPzM3JcqfOaV+gXA0szcD1gAXNjKY5EkDa7V01xnUIXFg2X5ceDszFydmX3A7cAeEfECYFJm3ljaLQSOj4iJwJHA4sZ6eX0c1cgE4HLg2NJektRmLZ3myszTASKif/k+4L5S6wHmAKcBuwErGjZdAewO7AyszswNA+o0blOmw1YDPTwVXJKkNmn5OZPNiYjnAUuAr2fmzyLicKpzKP26gF6qkVPfgM17G9o06mpYt1XTpk0eUp+lZvX0TOl0F6S2a3uYRMS+wLXARZn5P0p5OTC9odmuVCOMh4EdImJCZm4sbfpHHg+UdssjYhtgCrCq2X6sWrWW3t6BOdU8vzA0mJUr13S6C9KI6+7u2uIf4W29NDgipgA/AuY2BEn/9Ne6MkIBOBlYkpnrgaXArFI/hWpEA3B1WaasX1raS5LarN0jk9OB5wIfjIgPltoPMvM8YDawoFxKfAtwUVl/NnBZRMwF7gdOLPV5wMKIuBN4tGwvSeqArr6+4U/1jFJ7AveMxDTXtce8bsQ6pbHhmGuvcZpLY1LDNNdewL2brG93hyRJY49hIkmqzTCRJNVmmEiSajNMJEm1GSaSpNoME0lSbYaJJKk2w0SSVJthIkmqzTCRJNVmmEiSajNMJEm1GSaSpNoME0lSbYaJJKk2w0SSVJthIkmqzTCRJNVmmEiSajNMJEm1bdPqHUTEVOAG4PWZeW9EHA18HpgEXJGZc0u7mcAlwFTgOuCszNwQEXsAi4BdgARmZ+baiNgR+DawN7ASOCEzH2r18UiSNtXSkUlEvAy4HphRlicBlwJvAvYDDo2IY0vzRcCczJwBdAFnlPp8YH5m7gssA+aV+gXA0szcD1gAXNjKY5EkDa7V01xnAO8FHizLhwG/y8x7MnMDVYAcHxEvACZl5o2l3cJSnwgcCSxurJfXx1GNTAAuB44t7SVJbdbSMMnM0zNzaUNpN2BFw/IKYPct1HcGVpfgaaw/7b3K+tVAz0gfgyRp61p+zmSAbqCvYbkL6B1CnVLvb9Ooq2HdVk2bNrnZptKQ9PRM6XQXpLZrd5gsB6Y3LO9KNQU2WP1hYIeImJCZG0ub/imzB0q75RGxDTAFWNVsR1atWktv78Ccap5fGBrMypVrOt0FacR1d3dt8Y/wdl8afBMQEbFPREwATgKWZOZ9wLqIOLy0O7nU1wNLgVmlfgqwpLy+uixT1i8t7SVJbdbWMMnMdcBpwHeBu4C7eerk+mzgCxFxNzAZuKjUzwbOjIi7gCOAuaU+D3h5RNxZ2ry3HccgSdpUV1/f8Kd6Rqk9gXtGYprr2mNeN2Kd0thwzLXXOM2lMalhmmsv4N5N1re7Q5KksccwkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTatunETiPiHcDHyuKSzPxQRMwELgGmAtcBZ2XmhojYA1gE7AIkMDsz10bEjsC3gb2BlcAJmflQu49FktSBkUlEPBu4CHgV8BLgiIg4miow5mTmDKALOKNsMh+Yn5n7AsuAeaV+AbA0M/cDFgAXtu8oJEmNmgqTiPj6ZmqLh7nPCWW/2wMTy7/1wKTMvLG0WQgcHxETgSOBxY318vo4qpEJwOXAsaW9JKnNtjjNFRFfAZ5HNXroaVg1kWp6acgyc01EzAPuBv4M/Bx4AljR0GwFsDuwM7A6MzcMqAPs1r9NmQ5bDfQADw6nX5Kk4dvaOZOvAwdQTUd9t6G+Abhxs1tsRUQcCLwLeAHwR6rprb8C+hqadQG9VCOYvgFv0dvQplFXw7qtmjZtcvOdloagp2dKp7sgtd0WwyQzlwHLIuLHmbl8hPZ5DPCTzHwYICIWAh8Cpje02ZVqhPEwsENETMjMjaVN/8jjgdJueURsA0wBVjXbiVWr1tLbOzCnmucXhgazcuWaTndBGnHd3V1b/CO82RPwz4+If42I2yLi1/3/htmn24CjI2L7iOgC3kA11bUuIg4vbU6musprPbAUmFXqpwBLyuuryzJl/dLSXpLUZs1eGvw1qpPft7DptNOQZOaPIuKlwK+oTrzfDHwGuBJYEBFTy34uKpucDVwWEXOB+4ETS30esDAi7gQeBWbX6Zckafi6+vq2ng0RcUtmHtSG/rTDnsA9IzHNde0xrxuxTmlsOObaa5zm0pjUMM21F3DvJuubfJ87IuLFI9gvSdIY0uw0197AryLiPuCx/mJmHtiSXkmSRpVmw+TjLe2FJGlUazZMbm9pLyRJo1qzYfJ7qqu4unjqaq7GX6NLksaxpsIkM588UR8R2wInAdGqTkmSRpch3zU4M5/IzIXAa0e+O5Kk0aipkUlEPKdhsQs4BNipJT2SJI06wzlnAtU9s85pSY8kSaPOkM+ZSHrmmzJ1O7Z7lo/30dOte3w9a1ava8l7NzvN1U11Z99jqZ5l8iPg7xueMyLpGWS7Z03k7XM3eaadxrnFF7ybNbQmTJodcXwaeA3Vo3E/D7wS+FxLeiRJGnWaPWfyOuCQ/lu8R8RVVLeS/2+t6pgkafRodmTS3fiskMx8nOr28ZIkNT0yuTUivgB8meqqrvcBw304liRpjGl2ZPJeqt+V3ADcBOxMFSiSJG15ZFJunbIA+F5mnlZqVwEbgdUt750kaVTY2sjkk8BU4BcNtTOAHYHzW9QnSdIos7UweT1wUmY+3F/IzAeBU4C3tLJjkqTRY2th8kRmPjawmJmrgcdb0yVJ0miztTDZGBFTBhZLzXs1SJKArV8afDlwSUS8KzP/BBAR2wOXAN8d7k4j4g3AJ4DtgR9l5vsj4miqX9dPAq7IzLml7cyyv6nAdcBZmbkhIvYAFgG7AAnMzsy1w+2TJGn4tjYy+SLwR+ChiLgxIm4GHgIeoTo5P2QRsTfwVeDNwIHAQRFxLHAp8CZgP+DQUoMqMOZk5gyquxafUerzgfmZuS+wDJg3nP5Ikurb4sgkM3uBMyPiU8DBQC9wU2auqLHPt1CNPJYDRMQs4IXA7zLznlJbBBwfEXcBkzLzxrLtQuDvIuIS4EiqQOqv/xz4SI1+SZKGqdlb0N8H3DdC+9wHeCIifgDsAfwQuJPqmfL9+p8vv9sg9Z2B1Q13LfZ59JLUQc3eTmWk93kkcBSwFvgB8BjVbVr6dVGNgrqbrFPqTZs2bfJQmktN6+nZ5JoV6RmjVZ/PToTJQ8CPM3MlQERcCRxP9av6frsCDwLLgembqT8M7BAREzJzY2nz4FA6sWrVWnp7B+ZR8/zC0GBWrlzT6S74+dSghvv57O7u2uIf4Z14guIPgWMiYseImED1wK3FQETEPqV2ErCkTK+ti4jDy7Ynl/p6YCkwq9RPAZa09SgkSU9qe5hk5k3AZ4HrgbuozsV8BTiN6nLju4C7qQIGYDbwhYi4G5gMXFTqZ1NdHHAXcAQwt02HIEkaoBPTXGTmpVSXAjf6CfCSzbS9DThsM/X7qM67SJI6rBPTXJKkMcYwkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSpNsNEklSbYSJJqs0wkSTVZphIkmozTCRJtRkmkqTatunkziPiH4CdM/O0iJgJXAJMBa4DzsrMDRGxB7AI2AVIYHZmro2IHYFvA3sDK4ETMvOhjhyIJI1zHRuZRMRfAqc2lBYBczJzBtAFnFHq84H5mbkvsAyYV+oXAEszcz9gAXBhWzouSdpER8IkIp4DfAr4+7L8AmBSZt5YmiwEjo+IicCRwOLGenl9HNXIBOBy4NjSXpLUZp0amXwN+DjwSFneDVjRsH4FsDuwM7A6MzcMqD9tm7J+NdDT2m5Lkjan7edMIuJ04D8y8ycRcVopdwN9Dc26gN7N1Cn1/jaNuhrWbdW0aZObbSoNSU/PlE53QRpUqz6fnTgBPwuYHhG3As8BJlMFxvSGNrsCDwIPAztExITM3FjaPFjaPFDaLY+IbYApwKpmO7Fq1Vp6ewfmVPP8wtBgVq5c0+ku+PnUoIb7+ezu7triH+Ftn+bKzNdm5gGZORM4D/hBZr4TWBcRh5dmJwNLMnM9sJQqgABOAZaU11eXZcr6paW9JKnNOnpp8ACzgQURMRW4Bbio1M8GLouIucD9wImlPg9YGBF3Ao+W7SVJHdDRMMnMhVRXaJGZtwGHbabNfcBRm6n/AXhjSzsoSWqKv4CXJNVmmEiSajNMJEm1GSaSpNoME0lSbYaJJKk2w0SSVJthIkmqzTCRJNVmmEiSajNMJEm1GSaSpNoME0lSbYaJJKk2w0SSVJthIkmqzTCRJNVmmEiSajNMJEm1GSaSpNoME0lSbYaJJKm2bTqx04j4BHBCWbwqMz8cEUcDnwcmAVdk5tzSdiZwCTAVuA44KzM3RMQewCJgFyCB2Zm5ts2HIkmiAyOTEhp/BbwUmAkcHBEnApcCbwL2Aw6NiGPLJouAOZk5A+gCzij1+cD8zNwXWAbMa99RSJIadWKaawXwwcx8IjPXA78BZgC/y8x7MnMDVYAcHxEvACZl5o1l24WlPhE4EljcWG/jMUiSGrR9misz7+x/HREvpJru+hJVyPRbAewO7DZIfWdgdQmexnrTpk2bPOS+S83o6ZnS6S5Ig2rV57Mj50wAImJ/4CrgXGAD1eikXxfQSzVy6muiTqk3bdWqtfT2DnyL5vmFocGsXLmm013w86lBDffz2d3dtcU/wjtyNVdEHA78BPhoZl4GLAemNzTZFXhwC/WHgR0iYkKpTy91SVIHdOIE/POB7wEnZeZ3SvmmalXsUwLiJGBJZt4HrCvhA3Byqa8HlgKzSv0UYEnbDkKS9DSdmOb6ELAd8PmI6K99FTgN+G5ZdzVPnVyfDSyIiKnALcBFpX42cFlEzAXuB05sR+clSZvqxAn49wPvH2T1SzbT/jbgsM3U7wOOGtHOSZKGxV/AS5JqM0wkSbUZJpKk2gwTSVJthokkqTbDRJJUm2EiSarNMJEk1WaYSJJqM0wkSbUZJpKk2gwTSVJthokkqTbDRJJUm2EiSarNMJEk1WaYSJJqM0wkSbUZJpKk2gwTSVJthokkqbZtOt2BOiLiJGAuMBH4YmZe3OEuSdK4NGpHJhHxPOBTwF8AM4EzI+JFne2VJI1Po3lkcjTw08z8A0BELAbeDnxyK9tNAOju7qrdge2e+9za76GxZyQ+WyOhZ8fJne6CnoGG+/ls2G7C5taP5jDZDVjRsLwCOKyJ7aYD7LTT9rU78KpvXlb7PTT2TJv2zPgS/8qHZnW6C3oGGoHP53Tg/w4sjuYw6Qb6Gpa7gN4mtvslcARV+GxsQb8kaSyaQBUkv9zcytEcJsupQqHfrsCDTWz3OHB9S3okSWPbJiOSfqM5TH4MnB8RPcCfgLcBZ3a2S5I0Po3aq7ky8wHg48C/ArcC/5iZN3e2V5I0PnX19fVtvZUkSVswakcmkqRnDsNEklSbYSJJqs0wkSTVNpovDVaLRMSewG+BuwasekNm/sdm2p8PkJnnt7pvUkRcDBwObAvsw1Of0wsz8xsd69g4Z5hoMA9m5sxOd0IaKDPfC0/+0fMzP6fPDIaJmhYRBwBfAiYDuwCfzsyvNqyfCFwKHFBK8zNzQUQ8F/ga8HyqW958LDN/3NbOa8wrI+SXA3tQfU5nAedn5s8agmdPP4+t4TkTDWa3iLi14d+5wOnABZl5KPBq4HMDtnkl8JzMfClwHE/d7uZC4NLMPBh4I/C1iJjSnsPQOLNdZr4oM7+yhTZ+HlvAkYkGs8k0V0RMAF4XER8DXkw1Qml0R9UsrgWuBs4t9aOBfSOi//EAE4H/QnXnAmkk3dREGz+PLWCYaCj+CXgE+GfgO8CJjSszc1VE7A+8Fvhr4JayPAF4TcOzZ6YDD7ez4xo3Hmt43Ud1N3GoAqOfn8cWcJpLQ/Fa4LzM/D5wLDw5WqG8fiPwLeAq4BxgLdW89E+Bs0ubF1GNYJ7d1p5rPPo9sH95/eaGup/HFjBMNBTnA9dHxF1U50PuBfZqWL+E6i/DO4GbgUWZeTvwPuDlEfFr4ArgHZm5po391vj0WeDsiLgFmNRQ9/PYAt7oUZJUmyMTSVJthokkqTbDRJJUm2EiSarNMJEk1WaYaFyJiKMi4o427GdhRHxokHXnRcSbWt2HQfZ9R0QcNcxtd4iIn45wlzRGGCZS+72Gp/8ie7TYCTis053QM5O3U9F4NDkiFlM9C+NR4EzgP4GLgZlUt+FYAvxtZm6IiD6gJzN/D9C4HBEfBd4NrAGuA96cmXuW/bwyIm4Ankv1K+uTgNOAQ4DPRcTGzLxysE5GxAbgM1R3G9i+9Od/R8RpZZ/bA3/MzFdHxDyq29tsoHoWzZzMfKj8wvtSql9431226b99+x2ZOXmQ5Y8Bp5b3+13p9zeASRFxK3BwZm4cyn90jW2OTDQePR/4fLmR5T9S3QLmImAV1Q0sDwFeAmx2mqpfRBxD9SV7KHAwMPDOs8+juqngDGB34K2ZeTGwDDh3S0FSTAD+XO5uewJwaUT0lHX7A0eVIHknVeAcmpkHUgXXwtLu28CCUr8QeMFW9tl/W5zTgFdk5gHAPcAc4J3AY5k50yDRQIaJxqNfZ+YN5fVCqvB4I/DlzOzLzMeBr1LuP7YFfw38r8x8NDP7qEY2jb6XmX8uX7x3UD0DZqi+DJCZvwZuB45sOIbV5fWxwDcy809l+ULgL8tzOw4Evlne4xelH1tzdDmuR8p2H8jMTw2j7xpHnObSeDTwr+q+hn/9unn6eY0ugIjYtqG2gafuSru5910/YB9dDN2GAX3q38fahvoENu174/+3G/fb/34D+zPwuJ58v4jYEdhxSL3WuOPIROPRSyKi/1kt7wGupzpHMiciuiLiWVTnUf6ltFlJNXqB6rxHv6uAt0XEDmX53Tz9S30wG2j+BPwpABFxELAv8PPNtLkGeFdEbF+WzwGuy8z/BH5F9VCz/vd4cWnzKLBtOacCT3+cwI+Bt0bE1LJ8PvCB0u8JETGcUNQYZ5hoPPoN8ImIuI1qeutUqi/gXaimkm4HEuif2jkHuLjcfXY/YAVAZv4UWAD8W0QsA3YA/tzE/n8AfDoiTm2i7eFlv5cCs/qnngb4OlUA3BwRvwEOAmaXdScCfxMRtwPzyrGTmX8EPgwsiYhf0vAckMy8mupk+y/KdrsCHy/HfTNwZ0RMa6LvGke8a7A0TBFxCPDKzLyoLH8AeFlmzhqh93/aVWTSM5nnTKTh+y3wkYg4k2p6636q6bGmRMS5PDWCGOhz9bsntY8jE0lSbZ4zkSTVZphIkmozTCRJtRkmkqTaDBNJUm2GiSSptv8POPOJNC/tUe8AAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "lt.plot.distribution();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Count by Time\n", + "\n", + "This plot shows the label distribution across cutoff times." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "lt.plot.count_by_time();" + ] } ], "metadata": { diff --git a/docs/source/getting_started.ipynb b/docs/source/getting_started.ipynb index 2048871d..43b4c9a5 100644 --- a/docs/source/getting_started.ipynb +++ b/docs/source/getting_started.ipynb @@ -10,8 +10,24 @@ "Getting Started\n", "===============\n", "\n", - "In this example, we will generate labels on a mock dataset of transactions. For each customer, we want to label whether the total purchase amount over the next hour of transactions will exceed $300. Additionally, we want to predict one hour in advance.\n", - "\n", + "In this example, we will generate labels on a mock dataset of transactions. For each customer, we want to label whether the total purchase amount over the next hour of transactions will exceed $300. Additionally, we want to predict one hour in advance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import composeml as cp" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ "Load Data\n", "=========\n", "\n", @@ -24,8 +40,6 @@ "metadata": {}, "outputs": [], "source": [ - "import composeml as cp\n", - "\n", "df = cp.demos.load_transactions()\n", "\n", "df[df.columns[:7]].head()" @@ -192,18 +206,41 @@ "\n", "Also, there are plots available for insight to the labels.\n", "\n", - ".. code-block:: python\n", "\n", - " import matplotlib.pyplot as plt\n", + "Distribution\n", + "------------\n", "\n", - " fig, axs = plt.subplots(1,2)\n", - " fig.subplots_adjust(wspace=.34)\n", + "This plot shows the label distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "labels.plot.distribution();" + ] + }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "Count by Time\n", + "-------------\n", "\n", - " color = ['#4285F4', '#DB4437']\n", - " labels.plot.distribution(color=color, ax=axs[0])\n", - " labels.plot.count_by_time(figsize=(12, 4), color=color, ax=axs[1]);\n", - " \n", - ".. image:: images/getting_started_0.0.png\n" + "This plot shows the label distribution across cutoff times." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "labels.plot.count_by_time();" ] } ], diff --git a/docs/source/guides/using_label_transforms.ipynb b/docs/source/guides/using_label_transforms.ipynb index 7711ff48..0fe2106e 100644 --- a/docs/source/guides/using_label_transforms.ipynb +++ b/docs/source/guides/using_label_transforms.ipynb @@ -49,6 +49,7 @@ "labels = label_maker.search(\n", " cp.demos.load_transactions(),\n", " num_examples_per_instance=10,\n", + " label_type='continuous',\n", " minimum_data='2h',\n", " gap='2min',\n", " verbose=True,\n", diff --git a/requirements.txt b/requirements.txt index c1547c10..6ebc7499 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pandas>=0.23.0 numpy>=1.13.3 tqdm>=4.19.2 -matplotlib>=3.0.2 \ No newline at end of file +matplotlib>=3.0.2 +seaborn>=0.9.0