diff --git a/gramex/apps/mlhandler/template.html b/gramex/apps/mlhandler/template.html index 86d75e1c1..6f7eb14a0 100644 --- a/gramex/apps/mlhandler/template.html +++ b/gramex/apps/mlhandler/template.html @@ -73,6 +73,7 @@ 'DecisionTreeRegressor', 'RandomForestRegressor', 'MLPRegressor'] %} + {% set GRAINSIGHT_MODELS = ['TopCause'] %} {% set tcol = handler.get_opt('target_col', False) %} {% set CLASSIFICTION_METRICS = { 'Accuracy': 'accuracy', @@ -121,7 +122,7 @@

Train the Model

- {% for col in columns %} {% set selected = "selected" if col in handler.get_opt('cats', []) else "" %} @@ -130,7 +131,7 @@

Train the Model

- {% for col in columns %} {% set selected = "selected" if col in handler.get_opt('nums', []) else "" %} @@ -141,12 +142,12 @@

Train the Model

-
- {% if handler.get_opt('class') in CLASSIFICTION_MODELS %} {% for i, (mname, metric) in enumerate(CLASSIFICTION_METRICS.items()) %} {% set selected = "selected" if metric == "accuracy" else "" %} @@ -181,6 +182,12 @@

Train the Model

{% end %} + + {% for model in GRAINSIGHT_MODELS %} + {% set selected = "selected" if model == handler.get_opt('class') else "" %} + + {% end %} +
@@ -190,6 +197,7 @@

Train the Model

Results
+
@@ -234,7 +242,7 @@

Train the Model

Make Predictions

- +

@@ -246,7 +254,7 @@

<% COLS.forEach(function(col) { %>
- +
<% }) %>
@@ -276,34 +284,64 @@

if (s > 90) { color = '#00f700' } return color } - const get_score = function() { - let url = g1.url.parse(window.location) - $.ajax({ - url: url + '?_action=score&_metric=' + encodeURIComponent($('#metric').val()), - method: 'POST', - success: function(resp) { - let score = Number.parseFloat(JSON.parse(resp).score * 100).toPrecision(4) - score = Number.parseFloat(score) - $('.donut-segment').attr('stroke-dasharray', `${score} ${100 - score}`) - $('tspan').html(`${score}%`) - $('.donut-segment').attr('stroke', get_score_color(score)) - $('#resultcnt').show() - let inputcols = fh_meta.meta.columns.filter((col) => !$('#exclude').val().concat($('#targetcol').val()).includes(col.name)) - $('#predicttabtemplate').template({COLS: inputcols.map(e => ({name: e.name, type: e.type})), row: fh_meta.formdata[0]}) - } - }) + const get_score = function(resp) { + if ($('#modelchoice').val() != 'TopCause') { + $('#tcresult').hide() + let url = g1.url.parse(window.location) + $.ajax({ + url: url + '?_action=score&_metric=' + encodeURIComponent($('#metric').val()), + method: 'POST', + success: function(resp) { + let score = (resp.score * 100).toPrecision(4) + score = Number.parseFloat(score) + $('.donut-segment').attr('stroke-dasharray', `${score} ${100 - score}`) + $('tspan').html(`${score}%`) + $('.donut-segment').attr('stroke', get_score_color(score)) + $('#resultcnt').show() + let inputcols = fh_meta.meta.columns.filter((col) => !$('#exclude').val().concat($('#targetcol').val()).includes(col.name)) + $('#predicttabtemplate') + .on('template', modifyModelChoice) + .template({COLS: inputcols.map(e => ({name: e.name, type: e.type})), row: fh_meta.formdata[0]}) + } + }) + } else { + $('#tcresult').show() + $('#tcresult').formhandler({data: resp.result_}) + } } const post_train = function (target_col) { $('#resultcnt').hide() let url = g1.url.parse(window.location) url.hash = '' + url = url + '?_action=retrain&target_col=' + encodeURIComponent(target_col) + if ($('#modelchoice').val() != 'TopCause') { + url = url + '&_metric=' + encodeURIComponent($('#metric').val()) + } $.ajax({ - url: url + '?_action=retrain&target_col=' + encodeURIComponent(target_col) + '&_metric=' + encodeURIComponent($('#metric').val()), + url: url, method: 'POST', success: get_score }) } + const modifyModelChoice = function() { + $('.tcdisable').attr('disabled', $('#modelchoice').val() == 'TopCause') + $('.selectpicker').selectpicker('refresh') + if ($('#modelchoice').val() == 'TopCause') { + let url = g1.url.parse(window.location) + url.hash = '' + $.getJSON(url + '?_params').done(function(result) { + $('#tcresult').show() + $('#tcresult').formhandler({data: result.attrs.result_}) + }) + } else { + $('#tcresult').hide() + } + } $(document).ready(function() { + // If TopCause is selected, enable only some inputs + $('#modelchoice').change(modifyModelChoice) + $('#modelchoice').trigger('change') + $('#downloadbtn').hide() $('#resultcnt').hide() let url = g1.url.parse(window.location) @@ -312,7 +350,9 @@

$('.formhandler').on('load', function(obj) { fh_meta = obj let inputcols = obj.meta.columns.filter((col) => !$('#exclude').val().concat($('#targetcol').val()).includes(col.name)) - $('#predicttabtemplate').template({COLS: inputcols.map(e => ({name: e.name, type: e.type})), row: obj.formdata[0]}) + $('#predicttabtemplate') + .on('template', modifyModelChoice) + .template({COLS: inputcols.map(e => ({name: e.name, type: e.type})), row: obj.formdata[0]}) }).formhandler({ pageSize: 5, export: false diff --git a/gramex/handlers/mlhandler.py b/gramex/handlers/mlhandler.py index c7e4d3326..362e49753 100644 --- a/gramex/handlers/mlhandler.py +++ b/gramex/handlers/mlhandler.py @@ -11,6 +11,7 @@ from gramex.handlers import FormHandler from gramex.http import NOT_FOUND, BAD_REQUEST from gramex.install import _mkdir, safe_rmtree +from gramex.ml import TopCause from gramex import cache import joblib import pandas as pd @@ -224,12 +225,19 @@ def _filterrows(cls, data, **kwargs): @classmethod def _assemble_pipeline(cls, data, force=False, mclass='', params=None): + if params is None: + params = {} # If the model exists, return it if op.exists(cls.model_path) and not force: return joblib.load(cls.model_path) + model_kwargs = cls.config_store.load('model', {}) + if not mclass: + mclass = model_kwargs.get('class', False) + + # No pipeline for TopCause # If preprocessing is not enabled, return the root model - if not cls.get_opt('pipeline', True): + if mclass == 'TopCause' or not cls.get_opt('pipeline', True): return search_modelclass(mclass)(**params) # Else assemble the preprocessing pipeline @@ -252,8 +260,6 @@ def _assemble_pipeline(cls, data, force=False, mclass='', params=None): [('ohe', OneHotEncoder(sparse=False), categoricals), ('scaler', StandardScaler(), numericals)] ) - model_kwargs = cls.config_store.load('model', {}) - mclass = model_kwargs.get('class', False) if mclass: model = search_modelclass(mclass)(**model_kwargs.get('params', {})) cls.set_opt('params', model.get_params()) @@ -318,8 +324,9 @@ def get(self, *path_args, **path_kwargs): } try: model = cache.open(self.model_path, joblib.load) + estimator = model[-1] if hasattr(model, '__getitem__') else model attrs = { - k: v for k, v in vars(model[-1]).items() if re.search(r'[^_]+_$', k) + k: v for k, v in vars(estimator).items() if re.search(r'[^_]+_$', k) } except FileNotFoundError: attrs = {} @@ -367,17 +374,19 @@ def _train(self, data=None): data = self._filtercols(data) data = self._filterrows(data) self.model = self._assemble_pipeline(data, force=True) - if not isinstance(self.model[-1], TransformerMixin): + estimator = self.model[-1] if hasattr(self.model, '__getitem__') else self.model + if not isinstance(estimator, (TransformerMixin, TopCause)): target = data[target_col] train = data[[c for c in data if c != target_col]] _fit(self.model, train, target, self.model_path) result = {'score': self.model.score(train, target)} else: - _fit(self.model, data, path=self.model_path) + target = data[target_col] if isinstance(estimator, TopCause) else None + _fit(self.model, data, target, self.model_path) # Note: Fitted sklearn estimators store their parameters # in attributes whose names end in an underscore. E.g. in the case of PCA, # attributes are named `explained_variance_`. The `_train` action returns them. - result = {k: v for k, v in vars(self.model[-1]).items() if re.search(r'[^_]+_$', k)} + result = {k: v for k, v in vars(estimator).items() if re.search(r'[^_]+_$', k)} return result def _retrain(self): diff --git a/gramex/topcause.py b/gramex/topcause.py index 0944808e4..f0007f88d 100644 --- a/gramex/topcause.py +++ b/gramex/topcause.py @@ -176,6 +176,6 @@ def fit(self, X, y, sample_weight=None): # noqa - capital X is a sklearn conv results = pd.DataFrame(results).T results.loc[results['p'] > self.max_p, ('value', 'gain')] = np.nan - self.result_ = results.sort_values('gain', ascending=False) + self.result_ = results.sort_values('gain', ascending=False).reset_index() return self