From 8d34dd75115c69f66a842f67f38b29ed9decd881 Mon Sep 17 00:00:00 2001 From: Doris Lee Date: Sat, 2 Apr 2022 15:22:12 -0700 Subject: [PATCH] FEAT-#474: basic working example with mock recommendation and display Signed-off-by: Doris Lee --- lux/__init__.py | 28 +- lux/core/__init__.py | 108 +-- lux/core/frame.py | 1728 ++++++++++++++++++----------------- lux/core/old_frame.py | 874 ++++++++++++++++++ test_nb/refactor test.ipynb | 152 +++ 5 files changed, 1982 insertions(+), 908 deletions(-) create mode 100644 lux/core/old_frame.py create mode 100644 test_nb/refactor test.ipynb diff --git a/lux/__init__.py b/lux/__init__.py index 63459e96..b864ac70 100644 --- a/lux/__init__.py +++ b/lux/__init__.py @@ -13,21 +13,21 @@ # limitations under the License. # Register the commonly used modules (similar to how pandas does it: https://github.com/pandas-dev/pandas/blob/master/pandas/__init__.py) -from lux.vis.Clause import Clause -from lux.core.frame import LuxDataFrame -from lux.core.sqltable import LuxSQLTable -from lux.core.joinedsqltable import JoinedSQLTable -from lux.utils.tracing_utils import LuxTracer -from ._version import __version__, version_info -from lux._config import config -from lux._config.config import warning_format -from lux.utils.debug_utils import debug_info, check_luxwidget_enabled +# from lux.vis.Clause import Clause +# from lux.core.frame import LuxDataFrame +# from lux.core.sqltable import LuxSQLTable +# from lux.core.joinedsqltable import JoinedSQLTable +# from lux.utils.tracing_utils import LuxTracer +# from ._version import __version__, version_info +# from lux._config import config +# from lux._config.config import warning_format +# from lux.utils.debug_utils import debug_info, check_luxwidget_enabled -from lux._config import Config +# from lux._config import Config -config = Config() +# config = Config() -from lux.action.default import register_default_actions +# from lux.action.default import register_default_actions -register_default_actions() -check_luxwidget_enabled() +# register_default_actions() +# check_luxwidget_enabled() diff --git a/lux/core/__init__.py b/lux/core/__init__.py index d01b1976..c79e9768 100644 --- a/lux/core/__init__.py +++ b/lux/core/__init__.py @@ -12,62 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pandas as pd -from .frame import LuxDataFrame -from .groupby import LuxDataFrameGroupBy, LuxSeriesGroupBy -from .series import LuxSeries +# import pandas as pd +# from .frame import LuxDataFrame +# from .groupby import LuxDataFrameGroupBy, LuxSeriesGroupBy +# from .series import LuxSeries -global originalDF -# Keep variable scope of original pandas df -originalDF = pd.core.frame.DataFrame -originalSeries = pd.core.series.Series +# global originalDF +# # Keep variable scope of original pandas df +# originalDF = pd.core.frame.DataFrame +# originalSeries = pd.core.series.Series -def setOption(overridePandas=True): - if overridePandas: - pd.DataFrame = ( - pd.io.json._json.DataFrame - ) = ( - pd.io.sql.DataFrame - ) = ( - pd.io.excel.DataFrame - ) = ( - pd.io.formats.DataFrame - ) = ( - pd.io.sas.DataFrame - ) = ( - pd.io.clipboards.DataFrame - ) = ( - pd.io.common.DataFrame - ) = ( - pd.io.feather_format.DataFrame - ) = ( - pd.io.gbq.DataFrame - ) = ( - pd.io.html.DataFrame - ) = ( - pd.io.orc.DataFrame - ) = ( - pd.io.parquet.DataFrame - ) = ( - pd.io.pickle.DataFrame - ) = ( - pd.io.pytables.DataFrame - ) = ( - pd.io.spss.DataFrame - ) = ( - pd.io.stata.DataFrame - ) = pd.io.api.DataFrame = pd.core.frame.DataFrame = pd._testing.DataFrame = LuxDataFrame - if pd.__version__ < "1.3.0": - pd.io.parsers.DataFrame = LuxDataFrame - else: - pd.io.parsers.readers.DataFrame = LuxDataFrame - pd.Series = pd.core.series.Series = pd.core.groupby.ops.Series = pd._testing.Series = LuxSeries - pd.core.groupby.generic.DataFrameGroupBy = LuxDataFrameGroupBy - pd.core.groupby.generic.SeriesGroupBy = LuxSeriesGroupBy - else: - pd.DataFrame = pd.io.parsers.DataFrame = pd.core.frame.DataFrame = originalDF - pd.Series = originalSeries +# def setOption(overridePandas=True): +# if overridePandas: +# pd.DataFrame = ( +# pd.io.json._json.DataFrame +# ) = ( +# pd.io.sql.DataFrame +# ) = ( +# pd.io.excel.DataFrame +# ) = ( +# pd.io.formats.DataFrame +# ) = ( +# pd.io.sas.DataFrame +# ) = ( +# pd.io.clipboards.DataFrame +# ) = ( +# pd.io.common.DataFrame +# ) = ( +# pd.io.feather_format.DataFrame +# ) = ( +# pd.io.gbq.DataFrame +# ) = ( +# pd.io.html.DataFrame +# ) = ( +# pd.io.orc.DataFrame +# ) = ( +# pd.io.parquet.DataFrame +# ) = ( +# pd.io.pickle.DataFrame +# ) = ( +# pd.io.pytables.DataFrame +# ) = ( +# pd.io.spss.DataFrame +# ) = ( +# pd.io.stata.DataFrame +# ) = pd.io.api.DataFrame = pd.core.frame.DataFrame = pd._testing.DataFrame = LuxDataFrame +# if pd.__version__ < "1.3.0": +# pd.io.parsers.DataFrame = LuxDataFrame +# else: +# pd.io.parsers.readers.DataFrame = LuxDataFrame +# pd.Series = pd.core.series.Series = pd.core.groupby.ops.Series = pd._testing.Series = LuxSeries +# pd.core.groupby.generic.DataFrameGroupBy = LuxDataFrameGroupBy +# pd.core.groupby.generic.SeriesGroupBy = LuxSeriesGroupBy +# else: +# pd.DataFrame = pd.io.parsers.DataFrame = pd.core.frame.DataFrame = originalDF +# pd.Series = originalSeries -setOption(overridePandas=True) +# setOption(overridePandas=True) diff --git a/lux/core/frame.py b/lux/core/frame.py index 4d8da0e8..0df31091 100644 --- a/lux/core/frame.py +++ b/lux/core/frame.py @@ -13,862 +13,910 @@ # limitations under the License. import pandas as pd -from lux.core.series import LuxSeries +# from lux.core.series import LuxSeries from lux.vis.Clause import Clause -from lux.vis.Vis import Vis -from lux.vis.VisList import VisList -from lux.history.history import History -from lux.utils.date_utils import is_datetime_series -from lux.utils.message import Message -from lux.utils.utils import check_import_lux_widget +# from lux.vis.Vis import Vis +# from lux.vis.VisList import VisList +# from lux.history.history import History +# from lux.utils.date_utils import is_datetime_series +# from lux.utils.message import Message +# from lux.utils.utils import check_import_lux_widget from typing import Dict, Union, List, Callable -# from lux.executor.Executor import * import warnings import traceback import lux -class LuxDataFrame(pd.DataFrame): - """ - A subclass of pd.DataFrame that supports all dataframe operations while housing other variables and functions for generating visual recommendations. - """ - - # MUST register here for new properties!! - _metadata = [ - "_intent", - "_inferred_intent", - "_data_type", - "unique_values", - "cardinality", - "_rec_info", - "_min_max", - "_current_vis", - "_widget", - "_recommendation", - "_prev", - "_history", - "_saved_export", - "_sampled", - "_toggle_pandas_display", - "_message", - "_pandas_only", - "pre_aggregated", - "_type_override", - ] - - def __init__(self, *args, **kw): - self._history = History() - self._intent = [] - self._inferred_intent = [] - self._recommendation = {} - self._saved_export = None - self._current_vis = [] - self._prev = None - self._widget = None - super(LuxDataFrame, self).__init__(*args, **kw) - - self.table_name = "" - if lux.config.SQLconnection == "": - from lux.executor.PandasExecutor import PandasExecutor - - lux.config.executor = PandasExecutor() - else: - from lux.executor.SQLExecutor import SQLExecutor - - # lux.config.executor = SQLExecutor() - - self._sampled = None - self._approx_sample = None - self._toggle_pandas_display = True - self._message = Message() - self._pandas_only = False - # Metadata - self._data_type = {} - self.unique_values = None - self.cardinality = None - self._min_max = None - self.pre_aggregated = None - self._type_override = {} - warnings.formatwarning = lux.warning_format - - @property - def _constructor(self): - return LuxDataFrame - - @property - def _constructor_sliced(self): - def f(*args, **kwargs): - s = LuxSeries(*args, **kwargs) - for attr in self._metadata: # propagate metadata - s.__dict__[attr] = getattr(self, attr, None) - return s - - return f - - @property - def history(self): - return self._history - - @property - def data_type(self): - if not self._data_type: - self.maintain_metadata() - return self._data_type - - def compute_metadata(self) -> None: - """ - Compute dataset metadata and statistics - """ - if len(self) > 0: - if lux.config.executor.name != "SQLExecutor": - lux.config.executor.compute_stats(self) - lux.config.executor.compute_dataset_metadata(self) - self._infer_structure() - self._metadata_fresh = True - - def maintain_metadata(self): - """ - Maintain dataset metadata and statistics (Compute only if needed) - """ - is_sql_tbl = lux.config.executor.name != "PandasExecutor" - - if lux.config.SQLconnection != "" and is_sql_tbl: - from lux.executor.SQLExecutor import SQLExecutor - - # lux.config.executor = SQLExecutor() - - # Check that metadata has not yet been computed - if lux.config.lazy_maintain: - # Check that metadata has not yet been computed - if not hasattr(self, "_metadata_fresh") or not self._metadata_fresh: - # only compute metadata information if the dataframe is non-empty - self.compute_metadata() - else: - self.compute_metadata() - - def expire_recs(self) -> None: - """ - Expires and resets all recommendations - """ - if lux.config.lazy_maintain: - self._recs_fresh = False - self._recommendation = {} - self._widget = None - self._rec_info = None - self._sampled = None - - def expire_metadata(self) -> None: - """ - Expire all saved metadata to trigger a recomputation the next time the data is required. - """ - if lux.config.lazy_maintain: - self._metadata_fresh = False - self._data_type = None - self.unique_values = None - self.cardinality = None - self._min_max = None - self.pre_aggregated = None - - ##################### - ## Override Pandas ## - ##################### - def __getattr__(self, name): - ret_value = super(LuxDataFrame, self).__getattr__(name) - self.expire_metadata() - self.expire_recs() - return ret_value - - def _set_axis(self, axis, labels): - super(LuxDataFrame, self)._set_axis(axis, labels) - self.expire_metadata() - self.expire_recs() - - def _update_inplace(self, *args, **kwargs): - super(LuxDataFrame, self)._update_inplace(*args, **kwargs) - self.expire_metadata() - self.expire_recs() - - def _set_item(self, key, value): - super(LuxDataFrame, self)._set_item(key, value) - self.expire_metadata() - self.expire_recs() - - def _infer_structure(self): - # If the dataframe is very small and the index column is not a range index, then it is likely that this is an aggregated data - is_multi_index_flag = self.index.nlevels != 1 - not_int_index_flag = not pd.api.types.is_integer_dtype(self.index) - - is_sql_tbl = lux.config.executor.name != "PandasExecutor" - - small_df_flag = len(self) < 100 and is_sql_tbl - if self.pre_aggregated == None: - self.pre_aggregated = (is_multi_index_flag or not_int_index_flag) and small_df_flag - if "Number of Records" in self.columns: - self.pre_aggregated = True - self.pre_aggregated = "groupby" in [event.name for event in self.history] and not is_sql_tbl - - @property - def intent(self): - """ - Main function to set the intent of the dataframe. - The intent input goes through the parser, so that the string inputs are parsed into a lux.Clause object. - - Parameters - ---------- - intent : List[str,Clause] - intent list, can be a mix of string shorthand or a lux.Clause object - - Notes - ----- - :doc:`../guide/intent` - """ - return self._intent - - @intent.setter - def intent(self, intent_input: Union[List[Union[str, Clause]], Vis]): - is_list_input = isinstance(intent_input, list) - is_vis_input = isinstance(intent_input, Vis) - if not (is_list_input or is_vis_input): - raise TypeError( - "Input intent must be either a list (of strings or lux.Clause) or a lux.Vis object." - "\nSee more at: https://lux-api.readthedocs.io/en/latest/source/guide/intent.html" - ) - if is_list_input: - self.set_intent(intent_input) - elif is_vis_input: - self.set_intent_as_vis(intent_input) - - def clear_intent(self): - self.intent = [] - self.expire_recs() - - def set_intent(self, intent: List[Union[str, Clause]]): - self.expire_recs() - self._intent = intent - self._parse_validate_compile_intent() - - def _parse_validate_compile_intent(self): - self.maintain_metadata() - from lux.processor.Parser import Parser - from lux.processor.Validator import Validator - - self._intent = Parser.parse(self._intent) - Validator.validate_intent(self._intent, self) - self.maintain_metadata() - from lux.processor.Compiler import Compiler - - self.current_vis = Compiler.compile_intent(self, self._intent) - - def copy_intent(self): - # creates a true copy of the dataframe's intent - output = [] - for clause in self._intent: - temp_clause = clause.copy_clause() - output.append(temp_clause) - return output - - def set_intent_as_vis(self, vis: Vis): - """ - Set intent of the dataframe based on the intent of a Vis - - Parameters - ---------- - vis : Vis - Input Vis object - """ - self.expire_recs() - self._intent = vis._inferred_intent - self._parse_validate_compile_intent() - - def set_data_type(self, types: dict): - """ - Set the data type for a particular attribute in the dataframe - overriding the automatically-detected type inferred by Lux - - Parameters - ---------- - types: dict - Dictionary that maps attribute/column name to a specified Lux Type. - Possible options: "nominal", "quantitative", "id", and "temporal". - - Example - ---------- - df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/absenteeism.csv") - df.set_data_type({"ID":"id", - "Reason for absence":"nominal"}) - """ - if self._type_override == None: - self._type_override = types - else: - self._type_override = {**self._type_override, **types} - - if not self.data_type: - self.maintain_metadata() - - for attr in types: - if types[attr] not in ["nominal", "quantitative", "id", "temporal"]: - raise ValueError( - f'Invalid data type option specified for {attr}. Please use one of the following supported types: ["nominal", "quantitative", "id", "temporal"]' - ) - self.data_type[attr] = types[attr] - - self.expire_recs() - - def to_pandas(self): - import lux.core - - return lux.core.originalDF(self, copy=False) - - @property - def recommendation(self): - if self._recommendation is not None and self._recommendation == {}: - from lux.processor.Compiler import Compiler - - self.maintain_metadata() - self.current_vis = Compiler.compile_intent(self, self._intent) - self.maintain_recs() - return self._recommendation - - @recommendation.setter - def recommendation(self, recommendation: Dict): - self._recommendation = recommendation - - @property - def current_vis(self): - from lux.processor.Validator import Validator - - # _parse_validate_compile_intent does not call executor, - # we only attach data to current vis when user request current_vis - valid_current_vis = ( - self._current_vis is not None - and len(self._current_vis) > 0 - and self._current_vis[0].data is None - and self._current_vis[0].intent - ) - if valid_current_vis and Validator.validate_intent(self._current_vis[0].intent, self): - lux.config.executor.execute(self._current_vis, self) - return self._current_vis - - @current_vis.setter - def current_vis(self, current_vis: Dict): - self._current_vis = current_vis - - def _append_rec(self, rec_infolist, recommendations: Dict): - if recommendations["collection"] is not None and len(recommendations["collection"]) > 0: - rec_infolist.append(recommendations) - - def show_all_column_vis(self): - if len(self.columns) > 1 and len(self.columns) < 4 and self.intent == [] or self.intent is None: - vis = Vis(list(self.columns), self) - if vis.mark != "": - vis._all_column = True - self.current_vis = VisList([vis]) - - def maintain_recs(self, is_series="DataFrame"): - # `rec_df` is the dataframe to generate the recommendations on - # check to see if globally defined actions have been registered/removed - if lux.config.update_actions["flag"] == True: - self._recs_fresh = False - show_prev = False # flag indicating whether rec_df is showing previous df or current self - - if self._prev is not None: - rec_df = self._prev - rec_df._message = Message() - rec_df.maintain_metadata() # the prev dataframe may not have been printed before - last_event = self.history._events[-1].name - rec_df._message.add( - f"Lux is visualizing the previous version of the dataframe before you applied {last_event}." - ) - show_prev = True - else: - rec_df = self - rec_df._message = Message() - # Add warning message if there exist ID fields - if len(rec_df) == 0: - rec_df._message.add(f"Lux cannot operate on an empty {is_series}.") - elif len(rec_df) < 5 and not rec_df.pre_aggregated: - rec_df._message.add( - f"The {is_series} is too small to visualize. To generate visualizations in Lux, the {is_series} must contain at least 5 rows." - ) - elif self.index.nlevels >= 2 or self.columns.nlevels >= 2: - rec_df._message.add( - f"Lux does not currently support visualizations in a {is_series} " - f"with hierarchical indexes.\n" - f"Please convert the {is_series} into a flat " - f"table via pandas.DataFrame.reset_index." - ) - else: - id_fields_str = "" - inverted_data_type = lux.config.executor.invert_data_type(rec_df.data_type) - if len(inverted_data_type["id"]) > 0: - for id_field in inverted_data_type["id"]: - id_fields_str += f"{id_field}, " - id_fields_str = id_fields_str[:-2] - rec_df._message.add(f"{id_fields_str} is not visualized since it resembles an ID field.") - - rec_df._prev = None # reset _prev - - # If lazy, check that recs has not yet been computed - lazy_but_not_computed = lux.config.lazy_maintain and ( - not hasattr(rec_df, "_recs_fresh") or not rec_df._recs_fresh - ) - eager = not lux.config.lazy_maintain - - # Check that recs has not yet been computed - if lazy_but_not_computed or eager: - is_sql_tbl = lux.config.executor.name == "SQLExecutor" - rec_infolist = [] - from lux.action.row_group import row_group - from lux.action.column_group import column_group - - # TODO: Rewrite these as register action inside default actions - if rec_df.pre_aggregated: - if rec_df.columns.name is not None: - rec_df._append_rec(rec_infolist, row_group(rec_df)) - rec_df._append_rec(rec_infolist, column_group(rec_df)) - elif not (len(rec_df) < 5 and not rec_df.pre_aggregated and not is_sql_tbl) and not ( - self.index.nlevels >= 2 or self.columns.nlevels >= 2 - ): - from lux.action.custom import custom_actions - - # generate vis from globally registered actions and append to dataframe - custom_action_collection = custom_actions(rec_df) - for rec in custom_action_collection: - rec_df._append_rec(rec_infolist, rec) - lux.config.update_actions["flag"] = False - - # Store _rec_info into a more user-friendly dictionary form - rec_df._recommendation = {} - for rec_info in rec_infolist: - action_type = rec_info["action"] - vlist = rec_info["collection"] - if len(vlist) > 0: - rec_df._recommendation[action_type] = vlist - rec_df._rec_info = rec_infolist - rec_df.show_all_column_vis() - if lux.config.render_widget: - self._widget = rec_df.render_widget() - # re-render widget for the current dataframe if previous rec is not recomputed - elif show_prev: - rec_df.show_all_column_vis() - if lux.config.render_widget: - self._widget = rec_df.render_widget() - self._recs_fresh = True - - ####################################################### - ############## LuxWidget Result Display ############### - ####################################################### - @property - def widget(self): - if self._widget: - return self._widget - - @property - def exported(self) -> Union[Dict[str, VisList], VisList]: - """ - Get selected visualizations as exported Vis List - - Notes - ----- - Convert the _selectedVisIdxs dictionary into a programmable VisList - Example _selectedVisIdxs : - - {'Correlation': [0, 2], 'Occurrence': [1]} - - indicating the 0th and 2nd vis from the `Correlation` tab is selected, and the 1st vis from the `Occurrence` tab is selected. - - Returns - ------- - Union[Dict[str,VisList], VisList] - When there are no exported vis, return empty list -> [] - When all the exported vis is from the same tab, return a VisList of selected visualizations. -> VisList(v1, v2...) - When the exported vis is from the different tabs, return a dictionary with the action name as key and selected visualizations in the VisList. -> {"Enhance": VisList(v1, v2...), "Filter": VisList(v5, v7...), ..} - """ - if self.widget is None: - warnings.warn( - "\nNo widget attached to the dataframe." - "Please assign dataframe to an output variable.\n" - "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", - stacklevel=2, - ) - return [] - exported_vis_lst = self._widget._selectedVisIdxs - exported_vis = [] - if exported_vis_lst == {}: - if self._saved_export: - return self._saved_export + +# pandas +DataFrame = pd.DataFrame +Series = pd.Series + + +# def _dataframe_constructor_sliced(self: DataFrame): +# def f(*args, **kwargs): +# s = Series(*args, **kwargs) +# for attr in self._metadata: # propagate metadata +# s.__dict__[attr] = getattr(self, attr, None) +# return s + +# return f + +# def _dataframe_constructor(self: DataFrame): +# def f(*args, **kwargs): +# s = DataFrame(*args, **kwargs) +# for attr in self._metadata: # propagate metadata +# s.__dict__[attr] = getattr(self, attr, None) +# return s +# return f + +def _mock_dataframe_display_(self: DataFrame): + if hasattr(self, "intent") and self.intent is not None and all(x in self for x in self.intent): + self[self.intent].plot() + else: + self.plot() +def _dataframe_ipython_display_(self: DataFrame): + from IPython.display import display + from IPython.display import clear_output + import ipywidgets as widgets + + try: + # if self._pandas_only: + # display(self.display_pandas()) + # self._pandas_only = False + # else: + # if not self.index.nlevels >= 2 or self.columns.nlevels >= 2: + # self.maintain_metadata() + + # if self._intent != [] and (not hasattr(self, "_compiled") or not self._compiled): + # from lux.processor.Compiler import Compiler + + # self.current_vis = Compiler.compile_intent(self, self._intent) + + # if lux.config.default_display == "lux": + # self._toggle_pandas_display = False + # else: + # self._toggle_pandas_display = True + + # # df_to_display.maintain_recs() # compute the recommendations (TODO: This can be rendered in another thread in the background to populate self._widget) + # self.maintain_recs() + + # MOCK COMPUTED RECOMMENDATION + self.recommendation={'Correlation': []} + + # MOCK DISPLAY RECOMMENDATION + _mock_dataframe_display_(self) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + if lux.config.pandas_fallback: warnings.warn( - "\nNo visualization selected to export.\n" - "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", + "\nUnexpected error in rendering Lux widget and recommendations. " + "Falling back to Pandas display.\n" + "Please report the following issue on Github: https://github.com/lux-org/lux/issues \n", stacklevel=2, ) - return [] - if len(exported_vis_lst) == 1 and "currentVis" in exported_vis_lst: - return self.current_vis - elif len(exported_vis_lst) > 1: - exported_vis = {} - if "currentVis" in exported_vis_lst: - exported_vis["Current Vis"] = self.current_vis - for export_action in exported_vis_lst: - if export_action != "currentVis": - exported_vis[export_action] = VisList( - list( - map( - self._recommendation[export_action].__getitem__, - exported_vis_lst[export_action], - ) - ) - ) - return exported_vis - elif len(exported_vis_lst) == 1 and ("currentVis" not in exported_vis_lst): - export_action = list(exported_vis_lst.keys())[0] - exported_vis = VisList( - list( - map( - self._recommendation[export_action].__getitem__, - exported_vis_lst[export_action], - ) - ) - ) - self._saved_export = exported_vis - return exported_vis + warnings.warn(traceback.format_exc()) + display(self.display_pandas()) else: - warnings.warn( - "\nNo visualization selected to export.\n" - "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", - stacklevel=2, - ) - return [] - - def remove_deleted_recs(self, change): - for action in self._widget.deletedIndices: - deletedSoFar = 0 - for index in self._widget.deletedIndices[action]: - self._recommendation[action].remove_index(index - deletedSoFar) - deletedSoFar += 1 - - def set_intent_on_click(self, change): - from IPython.display import display, clear_output - from lux.processor.Compiler import Compiler + raise - intent_action = list(self._widget.selectedIntentIndex.keys())[0] - vis = self._recommendation[intent_action][self._widget.selectedIntentIndex[intent_action][0]] - self.set_intent_as_vis(vis) +def display_widget(self:DataFrame): + # Observers(callback_function, listen_to_this_variable) + self._widget.observe(self.remove_deleted_recs, names="deletedIndices") + self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") - self.maintain_metadata() - self.current_vis = Compiler.compile_intent(self, self._intent) - self.maintain_recs() + button = widgets.Button( + description="Toggle Pandas/Lux", + layout=widgets.Layout(width="140px", top="5px"), + ) + self.output = widgets.Output() + display(button, self.output) + def on_button_clicked(b): with self.output: + if b: + self._toggle_pandas_display = not self._toggle_pandas_display clear_output() - display(self._widget) - - self._widget.observe(self.remove_deleted_recs, names="deletedIndices") - self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") - - def _ipython_display_(self): - from IPython.display import display - from IPython.display import clear_output - import ipywidgets as widgets - - try: - if self._pandas_only: - display(self.display_pandas()) - self._pandas_only = False - else: - if not self.index.nlevels >= 2 or self.columns.nlevels >= 2: - self.maintain_metadata() - - if self._intent != [] and (not hasattr(self, "_compiled") or not self._compiled): - from lux.processor.Compiler import Compiler - - self.current_vis = Compiler.compile_intent(self, self._intent) - - if lux.config.default_display == "lux": - self._toggle_pandas_display = False - else: - self._toggle_pandas_display = True - - # df_to_display.maintain_recs() # compute the recommendations (TODO: This can be rendered in another thread in the background to populate self._widget) - self.maintain_recs() - - # Observers(callback_function, listen_to_this_variable) - self._widget.observe(self.remove_deleted_recs, names="deletedIndices") - self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") - - button = widgets.Button( - description="Toggle Pandas/Lux", - layout=widgets.Layout(width="140px", top="5px"), - ) - self.output = widgets.Output() - display(button, self.output) - - def on_button_clicked(b): - with self.output: - if b: - self._toggle_pandas_display = not self._toggle_pandas_display - clear_output() - if self._toggle_pandas_display: - display(self.display_pandas()) - else: - # b.layout.display = "none" - display(self._widget) - # b.layout.display = "inline-block" - - button.on_click(on_button_clicked) - on_button_clicked(None) - - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - if lux.config.pandas_fallback: - warnings.warn( - "\nUnexpected error in rendering Lux widget and recommendations. " - "Falling back to Pandas display.\n" - "Please report the following issue on Github: https://github.com/lux-org/lux/issues \n", - stacklevel=2, - ) - warnings.warn(traceback.format_exc()) + if self._toggle_pandas_display: display(self.display_pandas()) else: - raise - - def display_pandas(self): - return self.to_pandas() - - def render_widget(self, renderer: str = "altair", input_current_vis=""): - """ - Generate a LuxWidget based on the LuxDataFrame - - Structure of widgetJSON: - - { - - 'current_vis': {}, - 'recommendation': [ - - { - - 'action': 'Correlation', - 'description': "some description", - 'vspec': [ - - {Vega-Lite spec for vis 1}, - {Vega-Lite spec for vis 2}, - ... - - ] - - }, - ... repeat for other actions - - ] - - } - - Parameters - ---------- - renderer : str, optional - Choice of visualization rendering library, by default "altair" - input_current_vis : lux.LuxDataFrame, optional - User-specified current vis to override default Current Vis, by default - - """ - check_import_lux_widget() - import luxwidget - - widgetJSON = self.to_JSON(self._rec_info, input_current_vis=input_current_vis) - return luxwidget.LuxWidget( - currentVis=widgetJSON["current_vis"], - recommendations=widgetJSON["recommendation"], - intent=LuxDataFrame.intent_to_string(self._intent), - message=self._message.to_html(), - config={"plottingScale": lux.config.plotting_scale}, - ) - - @staticmethod - def intent_to_JSON(intent): - from lux.utils import utils - - filter_specs = utils.get_filter_specs(intent) - attrs_specs = utils.get_attrs_specs(intent) - - intent = {} - intent["attributes"] = [clause.attribute for clause in attrs_specs] - intent["filters"] = [clause.attribute for clause in filter_specs] - return intent - - @staticmethod - def intent_to_string(intent): - if intent: - return ", ".join([clause.to_string() for clause in intent]) - else: - return "" - - def to_JSON(self, rec_infolist, input_current_vis=""): - widget_spec = {} - if self.current_vis: - lux.config.executor.execute(self.current_vis, self) - widget_spec["current_vis"] = LuxDataFrame.current_vis_to_JSON( - self.current_vis, input_current_vis - ) - else: - widget_spec["current_vis"] = {} - widget_spec["recommendation"] = [] - - # Recommended Collection - recCollection = LuxDataFrame.rec_to_JSON(rec_infolist) - widget_spec["recommendation"].extend(recCollection) - return widget_spec - - @staticmethod - def current_vis_to_JSON(vlist, input_current_vis=""): - current_vis_spec = {} - numVC = len(vlist) # number of visualizations in the vis list - if numVC == 1: - current_vis_spec = vlist[0].to_code(language=lux.config.plotting_backend, prettyOutput=False) - elif numVC > 1: - pass - if vlist[0]._all_column: - current_vis_spec["allcols"] = True - else: - current_vis_spec["allcols"] = False - return current_vis_spec - - @staticmethod - def rec_to_JSON(recs): - rec_lst = [] - import copy - - rec_copy = copy.deepcopy(recs) - for idx, rec in enumerate(rec_copy): - if len(rec["collection"]) > 0: - rec["vspec"] = [] - for vis in rec["collection"]: - chart = vis.to_code(language=lux.config.plotting_backend, prettyOutput=False) - rec["vspec"].append(chart) - rec_lst.append(rec) - # delete since not JSON serializable - del rec_lst[idx]["collection"] - return rec_lst - - def save_as_html(self, filename: str = "export.html", output=False): - """ - Save dataframe widget as static HTML file - - Parameters - ---------- - filename : str - Filename for the output HTML file - """ - - if self.widget is None: - self.maintain_metadata() - self.maintain_recs() - - from ipywidgets.embed import embed_data - - data = embed_data(views=[self.widget]) - - import json - - manager_state = json.dumps(data["manager_state"]) - widget_view = json.dumps(data["view_specs"][0]) - - # Separate out header since CSS file conflict with {} notation in Python format strings - header = """ - - - Lux Widget - - - - - - - - - - - """ - html_template = """ - - {header} - - - - - - - - - - - """ - - manager_state = json.dumps(data["manager_state"]) - widget_view = json.dumps(data["view_specs"][0]) - rendered_template = html_template.format( - header=header, manager_state=manager_state, widget_view=widget_view - ) - if output: - return rendered_template - else: - with open(filename, "w") as fp: - fp.write(rendered_template) - print(f"Saved HTML to {filename}") - - # Overridden Pandas Functions - def head(self, n: int = 5): - ret_val = super(LuxDataFrame, self).head(n) - ret_val._prev = self - ret_val._history.append_event("head", n=5) - return ret_val - - def tail(self, n: int = 5): - ret_val = super(LuxDataFrame, self).tail(n) - ret_val._prev = self - ret_val._history.append_event("tail", n=5) - return ret_val - - def groupby(self, *args, **kwargs): - history_flag = False - if "history" not in kwargs or ("history" in kwargs and kwargs["history"]): - history_flag = True - if "history" in kwargs: - del kwargs["history"] - groupby_obj = super(LuxDataFrame, self).groupby(*args, **kwargs) - for attr in self._metadata: - groupby_obj.__dict__[attr] = getattr(self, attr, None) - if history_flag: - groupby_obj._history = groupby_obj._history.copy() - groupby_obj._history.append_event("groupby", *args, **kwargs) - groupby_obj.pre_aggregated = True - return groupby_obj + # b.layout.display = "none" + display(self._widget) + # b.layout.display = "inline-block" + + button.on_click(on_button_clicked) + on_button_clicked(None) +# ------------------------------------------------------------------------------ +# Override Pandas +# ------------------------------------------------------------------------------ + +DataFrame._ipython_display_ = _dataframe_ipython_display_ +# DataFrame._constructor = _dataframe_constructor +# DataFrame._constructor_sliced = _dataframe_constructor_sliced +DataFrame._metadata = ["intent"] +# class LuxDataFrame(pd.DataFrame): +# """ +# A subclass of pd.DataFrame that supports all dataframe operations while housing other variables and functions for generating visual recommendations. +# """ + +# # MUST register here for new properties!! +# _metadata = [ +# "_intent", +# "_inferred_intent", +# "_data_type", +# "unique_values", +# "cardinality", +# "_rec_info", +# "_min_max", +# "_current_vis", +# "_widget", +# "_recommendation", +# "_prev", +# "_history", +# "_saved_export", +# "_sampled", +# "_toggle_pandas_display", +# "_message", +# "_pandas_only", +# "pre_aggregated", +# "_type_override", +# ] + +# def __init__(self, *args, **kw): +# self._history = History() +# self._intent = [] +# self._inferred_intent = [] +# self._recommendation = {} +# self._saved_export = None +# self._current_vis = [] +# self._prev = None +# self._widget = None +# super(LuxDataFrame, self).__init__(*args, **kw) + +# self.table_name = "" +# if lux.config.SQLconnection == "": +# from lux.executor.PandasExecutor import PandasExecutor + +# lux.config.executor = PandasExecutor() +# else: +# from lux.executor.SQLExecutor import SQLExecutor + +# # lux.config.executor = SQLExecutor() + +# self._sampled = None +# self._approx_sample = None +# self._toggle_pandas_display = True +# self._message = Message() +# self._pandas_only = False +# # Metadata +# self._data_type = {} +# self.unique_values = None +# self.cardinality = None +# self._min_max = None +# self.pre_aggregated = None +# self._type_override = {} +# warnings.formatwarning = lux.warning_format + +# @property +# def _constructor(self): +# return LuxDataFrame + +# @property +# def _constructor_sliced(self): +# def f(*args, **kwargs): +# s = LuxSeries(*args, **kwargs) +# for attr in self._metadata: # propagate metadata +# s.__dict__[attr] = getattr(self, attr, None) +# return s + +# return f + +# @property +# def history(self): +# return self._history + +# @property +# def data_type(self): +# if not self._data_type: +# self.maintain_metadata() +# return self._data_type + + + + + + + + + +# def compute_metadata(self) -> None: +# """ +# Compute dataset metadata and statistics +# """ +# if len(self) > 0: +# if lux.config.executor.name != "SQLExecutor": +# lux.config.executor.compute_stats(self) +# lux.config.executor.compute_dataset_metadata(self) +# self._infer_structure() +# self._metadata_fresh = True + +# def maintain_metadata(self): +# """ +# Maintain dataset metadata and statistics (Compute only if needed) +# """ +# is_sql_tbl = lux.config.executor.name != "PandasExecutor" + +# if lux.config.SQLconnection != "" and is_sql_tbl: +# from lux.executor.SQLExecutor import SQLExecutor + +# # lux.config.executor = SQLExecutor() + +# # Check that metadata has not yet been computed +# if lux.config.lazy_maintain: +# # Check that metadata has not yet been computed +# if not hasattr(self, "_metadata_fresh") or not self._metadata_fresh: +# # only compute metadata information if the dataframe is non-empty +# self.compute_metadata() +# else: +# self.compute_metadata() + +# def expire_recs(self) -> None: +# """ +# Expires and resets all recommendations +# """ +# if lux.config.lazy_maintain: +# self._recs_fresh = False +# self._recommendation = {} +# self._widget = None +# self._rec_info = None +# self._sampled = None + +# def expire_metadata(self) -> None: +# """ +# Expire all saved metadata to trigger a recomputation the next time the data is required. +# """ +# if lux.config.lazy_maintain: +# self._metadata_fresh = False +# self._data_type = None +# self.unique_values = None +# self.cardinality = None +# self._min_max = None +# self.pre_aggregated = None + +# ##################### +# ## Override Pandas ## +# ##################### +# def __getattr__(self, name): +# ret_value = super(LuxDataFrame, self).__getattr__(name) +# self.expire_metadata() +# self.expire_recs() +# return ret_value + +# def _set_axis(self, axis, labels): +# super(LuxDataFrame, self)._set_axis(axis, labels) +# self.expire_metadata() +# self.expire_recs() + +# def _update_inplace(self, *args, **kwargs): +# super(LuxDataFrame, self)._update_inplace(*args, **kwargs) +# self.expire_metadata() +# self.expire_recs() + +# def _set_item(self, key, value): +# super(LuxDataFrame, self)._set_item(key, value) +# self.expire_metadata() +# self.expire_recs() + +# def _infer_structure(self): +# # If the dataframe is very small and the index column is not a range index, then it is likely that this is an aggregated data +# is_multi_index_flag = self.index.nlevels != 1 +# not_int_index_flag = not pd.api.types.is_integer_dtype(self.index) + +# is_sql_tbl = lux.config.executor.name != "PandasExecutor" + +# small_df_flag = len(self) < 100 and is_sql_tbl +# if self.pre_aggregated == None: +# self.pre_aggregated = (is_multi_index_flag or not_int_index_flag) and small_df_flag +# if "Number of Records" in self.columns: +# self.pre_aggregated = True +# self.pre_aggregated = "groupby" in [event.name for event in self.history] and not is_sql_tbl + +# @property +# def intent(self): +# """ +# Main function to set the intent of the dataframe. +# The intent input goes through the parser, so that the string inputs are parsed into a lux.Clause object. + +# Parameters +# ---------- +# intent : List[str,Clause] +# intent list, can be a mix of string shorthand or a lux.Clause object + +# Notes +# ----- +# :doc:`../guide/intent` +# """ +# return self._intent + +# @intent.setter +# def intent(self, intent_input: Union[List[Union[str, Clause]], Vis]): +# is_list_input = isinstance(intent_input, list) +# is_vis_input = isinstance(intent_input, Vis) +# if not (is_list_input or is_vis_input): +# raise TypeError( +# "Input intent must be either a list (of strings or lux.Clause) or a lux.Vis object." +# "\nSee more at: https://lux-api.readthedocs.io/en/latest/source/guide/intent.html" +# ) +# if is_list_input: +# self.set_intent(intent_input) +# elif is_vis_input: +# self.set_intent_as_vis(intent_input) + +# def clear_intent(self): +# self.intent = [] +# self.expire_recs() + +# def set_intent(self, intent: List[Union[str, Clause]]): +# self.expire_recs() +# self._intent = intent +# self._parse_validate_compile_intent() + +# def _parse_validate_compile_intent(self): +# self.maintain_metadata() +# from lux.processor.Parser import Parser +# from lux.processor.Validator import Validator + +# self._intent = Parser.parse(self._intent) +# Validator.validate_intent(self._intent, self) +# self.maintain_metadata() +# from lux.processor.Compiler import Compiler + +# self.current_vis = Compiler.compile_intent(self, self._intent) + +# def copy_intent(self): +# # creates a true copy of the dataframe's intent +# output = [] +# for clause in self._intent: +# temp_clause = clause.copy_clause() +# output.append(temp_clause) +# return output + +# def set_intent_as_vis(self, vis: Vis): +# """ +# Set intent of the dataframe based on the intent of a Vis + +# Parameters +# ---------- +# vis : Vis +# Input Vis object +# """ +# self.expire_recs() +# self._intent = vis._inferred_intent +# self._parse_validate_compile_intent() + +# def set_data_type(self, types: dict): +# """ +# Set the data type for a particular attribute in the dataframe +# overriding the automatically-detected type inferred by Lux + +# Parameters +# ---------- +# types: dict +# Dictionary that maps attribute/column name to a specified Lux Type. +# Possible options: "nominal", "quantitative", "id", and "temporal". + +# Example +# ---------- +# df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/absenteeism.csv") +# df.set_data_type({"ID":"id", +# "Reason for absence":"nominal"}) +# """ +# if self._type_override == None: +# self._type_override = types +# else: +# self._type_override = {**self._type_override, **types} + +# if not self.data_type: +# self.maintain_metadata() + +# for attr in types: +# if types[attr] not in ["nominal", "quantitative", "id", "temporal"]: +# raise ValueError( +# f'Invalid data type option specified for {attr}. Please use one of the following supported types: ["nominal", "quantitative", "id", "temporal"]' +# ) +# self.data_type[attr] = types[attr] + +# self.expire_recs() + +# def to_pandas(self): +# import lux.core + +# return lux.core.originalDF(self, copy=False) + +# @property +# def recommendation(self): +# if self._recommendation is not None and self._recommendation == {}: +# from lux.processor.Compiler import Compiler + +# self.maintain_metadata() +# self.current_vis = Compiler.compile_intent(self, self._intent) +# self.maintain_recs() +# return self._recommendation + +# @recommendation.setter +# def recommendation(self, recommendation: Dict): +# self._recommendation = recommendation + +# @property +# def current_vis(self): +# from lux.processor.Validator import Validator + +# # _parse_validate_compile_intent does not call executor, +# # we only attach data to current vis when user request current_vis +# valid_current_vis = ( +# self._current_vis is not None +# and len(self._current_vis) > 0 +# and self._current_vis[0].data is None +# and self._current_vis[0].intent +# ) +# if valid_current_vis and Validator.validate_intent(self._current_vis[0].intent, self): +# lux.config.executor.execute(self._current_vis, self) +# return self._current_vis + +# @current_vis.setter +# def current_vis(self, current_vis: Dict): +# self._current_vis = current_vis + +# def _append_rec(self, rec_infolist, recommendations: Dict): +# if recommendations["collection"] is not None and len(recommendations["collection"]) > 0: +# rec_infolist.append(recommendations) + +# def show_all_column_vis(self): +# if len(self.columns) > 1 and len(self.columns) < 4 and self.intent == [] or self.intent is None: +# vis = Vis(list(self.columns), self) +# if vis.mark != "": +# vis._all_column = True +# self.current_vis = VisList([vis]) + +# def maintain_recs(self, is_series="DataFrame"): +# # `rec_df` is the dataframe to generate the recommendations on +# # check to see if globally defined actions have been registered/removed +# if lux.config.update_actions["flag"] == True: +# self._recs_fresh = False +# show_prev = False # flag indicating whether rec_df is showing previous df or current self + +# if self._prev is not None: +# rec_df = self._prev +# rec_df._message = Message() +# rec_df.maintain_metadata() # the prev dataframe may not have been printed before +# last_event = self.history._events[-1].name +# rec_df._message.add( +# f"Lux is visualizing the previous version of the dataframe before you applied {last_event}." +# ) +# show_prev = True +# else: +# rec_df = self +# rec_df._message = Message() +# # Add warning message if there exist ID fields +# if len(rec_df) == 0: +# rec_df._message.add(f"Lux cannot operate on an empty {is_series}.") +# elif len(rec_df) < 5 and not rec_df.pre_aggregated: +# rec_df._message.add( +# f"The {is_series} is too small to visualize. To generate visualizations in Lux, the {is_series} must contain at least 5 rows." +# ) +# elif self.index.nlevels >= 2 or self.columns.nlevels >= 2: +# rec_df._message.add( +# f"Lux does not currently support visualizations in a {is_series} " +# f"with hierarchical indexes.\n" +# f"Please convert the {is_series} into a flat " +# f"table via pandas.DataFrame.reset_index." +# ) +# else: +# id_fields_str = "" +# inverted_data_type = lux.config.executor.invert_data_type(rec_df.data_type) +# if len(inverted_data_type["id"]) > 0: +# for id_field in inverted_data_type["id"]: +# id_fields_str += f"{id_field}, " +# id_fields_str = id_fields_str[:-2] +# rec_df._message.add(f"{id_fields_str} is not visualized since it resembles an ID field.") + +# rec_df._prev = None # reset _prev + +# # If lazy, check that recs has not yet been computed +# lazy_but_not_computed = lux.config.lazy_maintain and ( +# not hasattr(rec_df, "_recs_fresh") or not rec_df._recs_fresh +# ) +# eager = not lux.config.lazy_maintain + +# # Check that recs has not yet been computed +# if lazy_but_not_computed or eager: +# is_sql_tbl = lux.config.executor.name == "SQLExecutor" +# rec_infolist = [] +# from lux.action.row_group import row_group +# from lux.action.column_group import column_group + +# # TODO: Rewrite these as register action inside default actions +# if rec_df.pre_aggregated: +# if rec_df.columns.name is not None: +# rec_df._append_rec(rec_infolist, row_group(rec_df)) +# rec_df._append_rec(rec_infolist, column_group(rec_df)) +# elif not (len(rec_df) < 5 and not rec_df.pre_aggregated and not is_sql_tbl) and not ( +# self.index.nlevels >= 2 or self.columns.nlevels >= 2 +# ): +# from lux.action.custom import custom_actions + +# # generate vis from globally registered actions and append to dataframe +# custom_action_collection = custom_actions(rec_df) +# for rec in custom_action_collection: +# rec_df._append_rec(rec_infolist, rec) +# lux.config.update_actions["flag"] = False + +# # Store _rec_info into a more user-friendly dictionary form +# rec_df._recommendation = {} +# for rec_info in rec_infolist: +# action_type = rec_info["action"] +# vlist = rec_info["collection"] +# if len(vlist) > 0: +# rec_df._recommendation[action_type] = vlist +# rec_df._rec_info = rec_infolist +# rec_df.show_all_column_vis() +# if lux.config.render_widget: +# self._widget = rec_df.render_widget() +# # re-render widget for the current dataframe if previous rec is not recomputed +# elif show_prev: +# rec_df.show_all_column_vis() +# if lux.config.render_widget: +# self._widget = rec_df.render_widget() +# self._recs_fresh = True + +# ####################################################### +# ############## LuxWidget Result Display ############### +# ####################################################### +# @property +# def widget(self): +# if self._widget: +# return self._widget + +# @property +# def exported(self) -> Union[Dict[str, VisList], VisList]: +# """ +# Get selected visualizations as exported Vis List + +# Notes +# ----- +# Convert the _selectedVisIdxs dictionary into a programmable VisList +# Example _selectedVisIdxs : + +# {'Correlation': [0, 2], 'Occurrence': [1]} + +# indicating the 0th and 2nd vis from the `Correlation` tab is selected, and the 1st vis from the `Occurrence` tab is selected. + +# Returns +# ------- +# Union[Dict[str,VisList], VisList] +# When there are no exported vis, return empty list -> [] +# When all the exported vis is from the same tab, return a VisList of selected visualizations. -> VisList(v1, v2...) +# When the exported vis is from the different tabs, return a dictionary with the action name as key and selected visualizations in the VisList. -> {"Enhance": VisList(v1, v2...), "Filter": VisList(v5, v7...), ..} +# """ +# if self.widget is None: +# warnings.warn( +# "\nNo widget attached to the dataframe." +# "Please assign dataframe to an output variable.\n" +# "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", +# stacklevel=2, +# ) +# return [] +# exported_vis_lst = self._widget._selectedVisIdxs +# exported_vis = [] +# if exported_vis_lst == {}: +# if self._saved_export: +# return self._saved_export +# warnings.warn( +# "\nNo visualization selected to export.\n" +# "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", +# stacklevel=2, +# ) +# return [] +# if len(exported_vis_lst) == 1 and "currentVis" in exported_vis_lst: +# return self.current_vis +# elif len(exported_vis_lst) > 1: +# exported_vis = {} +# if "currentVis" in exported_vis_lst: +# exported_vis["Current Vis"] = self.current_vis +# for export_action in exported_vis_lst: +# if export_action != "currentVis": +# exported_vis[export_action] = VisList( +# list( +# map( +# self._recommendation[export_action].__getitem__, +# exported_vis_lst[export_action], +# ) +# ) +# ) +# return exported_vis +# elif len(exported_vis_lst) == 1 and ("currentVis" not in exported_vis_lst): +# export_action = list(exported_vis_lst.keys())[0] +# exported_vis = VisList( +# list( +# map( +# self._recommendation[export_action].__getitem__, +# exported_vis_lst[export_action], +# ) +# ) +# ) +# self._saved_export = exported_vis +# return exported_vis +# else: +# warnings.warn( +# "\nNo visualization selected to export.\n" +# "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", +# stacklevel=2, +# ) +# return [] + +# def remove_deleted_recs(self, change): +# for action in self._widget.deletedIndices: +# deletedSoFar = 0 +# for index in self._widget.deletedIndices[action]: +# self._recommendation[action].remove_index(index - deletedSoFar) +# deletedSoFar += 1 + +# def set_intent_on_click(self, change): +# from IPython.display import display, clear_output +# from lux.processor.Compiler import Compiler + +# intent_action = list(self._widget.selectedIntentIndex.keys())[0] +# vis = self._recommendation[intent_action][self._widget.selectedIntentIndex[intent_action][0]] +# self.set_intent_as_vis(vis) + +# self.maintain_metadata() +# self.current_vis = Compiler.compile_intent(self, self._intent) +# self.maintain_recs() + +# with self.output: +# clear_output() +# display(self._widget) + +# self._widget.observe(self.remove_deleted_recs, names="deletedIndices") +# self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") + +# def display_pandas(self): +# return self.to_pandas() + +# def render_widget(self, renderer: str = "altair", input_current_vis=""): +# """ +# Generate a LuxWidget based on the LuxDataFrame + +# Structure of widgetJSON: + +# { + +# 'current_vis': {}, +# 'recommendation': [ + +# { + +# 'action': 'Correlation', +# 'description': "some description", +# 'vspec': [ + +# {Vega-Lite spec for vis 1}, +# {Vega-Lite spec for vis 2}, +# ... + +# ] + +# }, +# ... repeat for other actions + +# ] + +# } + +# Parameters +# ---------- +# renderer : str, optional +# Choice of visualization rendering library, by default "altair" +# input_current_vis : lux.LuxDataFrame, optional +# User-specified current vis to override default Current Vis, by default + +# """ +# check_import_lux_widget() +# import luxwidget + +# widgetJSON = self.to_JSON(self._rec_info, input_current_vis=input_current_vis) +# return luxwidget.LuxWidget( +# currentVis=widgetJSON["current_vis"], +# recommendations=widgetJSON["recommendation"], +# intent=LuxDataFrame.intent_to_string(self._intent), +# message=self._message.to_html(), +# config={"plottingScale": lux.config.plotting_scale}, +# ) + +# @staticmethod +# def intent_to_JSON(intent): +# from lux.utils import utils + +# filter_specs = utils.get_filter_specs(intent) +# attrs_specs = utils.get_attrs_specs(intent) + +# intent = {} +# intent["attributes"] = [clause.attribute for clause in attrs_specs] +# intent["filters"] = [clause.attribute for clause in filter_specs] +# return intent + +# @staticmethod +# def intent_to_string(intent): +# if intent: +# return ", ".join([clause.to_string() for clause in intent]) +# else: +# return "" + +# def to_JSON(self, rec_infolist, input_current_vis=""): +# widget_spec = {} +# if self.current_vis: +# lux.config.executor.execute(self.current_vis, self) +# widget_spec["current_vis"] = LuxDataFrame.current_vis_to_JSON( +# self.current_vis, input_current_vis +# ) +# else: +# widget_spec["current_vis"] = {} +# widget_spec["recommendation"] = [] + +# # Recommended Collection +# recCollection = LuxDataFrame.rec_to_JSON(rec_infolist) +# widget_spec["recommendation"].extend(recCollection) +# return widget_spec + +# @staticmethod +# def current_vis_to_JSON(vlist, input_current_vis=""): +# current_vis_spec = {} +# numVC = len(vlist) # number of visualizations in the vis list +# if numVC == 1: +# current_vis_spec = vlist[0].to_code(language=lux.config.plotting_backend, prettyOutput=False) +# elif numVC > 1: +# pass +# if vlist[0]._all_column: +# current_vis_spec["allcols"] = True +# else: +# current_vis_spec["allcols"] = False +# return current_vis_spec + +# @staticmethod +# def rec_to_JSON(recs): +# rec_lst = [] +# import copy + +# rec_copy = copy.deepcopy(recs) +# for idx, rec in enumerate(rec_copy): +# if len(rec["collection"]) > 0: +# rec["vspec"] = [] +# for vis in rec["collection"]: +# chart = vis.to_code(language=lux.config.plotting_backend, prettyOutput=False) +# rec["vspec"].append(chart) +# rec_lst.append(rec) +# # delete since not JSON serializable +# del rec_lst[idx]["collection"] +# return rec_lst + +# def save_as_html(self, filename: str = "export.html", output=False): +# """ +# Save dataframe widget as static HTML file + +# Parameters +# ---------- +# filename : str +# Filename for the output HTML file +# """ + +# if self.widget is None: +# self.maintain_metadata() +# self.maintain_recs() + +# from ipywidgets.embed import embed_data + +# data = embed_data(views=[self.widget]) + +# import json + +# manager_state = json.dumps(data["manager_state"]) +# widget_view = json.dumps(data["view_specs"][0]) + +# # Separate out header since CSS file conflict with {} notation in Python format strings +# header = """ +# + +# Lux Widget +# +# +# +# + +# +# + +# +# +# """ +# html_template = """ +# +# {header} +# + +# + +# + +# + +# +# +# """ + +# manager_state = json.dumps(data["manager_state"]) +# widget_view = json.dumps(data["view_specs"][0]) +# rendered_template = html_template.format( +# header=header, manager_state=manager_state, widget_view=widget_view +# ) +# if output: +# return rendered_template +# else: +# with open(filename, "w") as fp: +# fp.write(rendered_template) +# print(f"Saved HTML to {filename}") + +# Overridden Pandas Functions +# def head(self, n: int = 5): +# ret_val = super(LuxDataFrame, self).head(n) +# ret_val._prev = self +# ret_val._history.append_event("head", n=5) +# return ret_val + +# def tail(self, n: int = 5): +# ret_val = super(LuxDataFrame, self).tail(n) +# ret_val._prev = self +# ret_val._history.append_event("tail", n=5) +# return ret_val + +# def groupby(self, *args, **kwargs): +# history_flag = False +# if "history" not in kwargs or ("history" in kwargs and kwargs["history"]): +# history_flag = True +# if "history" in kwargs: +# del kwargs["history"] +# groupby_obj = super(LuxDataFrame, self).groupby(*args, **kwargs) +# for attr in self._metadata: +# groupby_obj.__dict__[attr] = getattr(self, attr, None) +# if history_flag: +# groupby_obj._history = groupby_obj._history.copy() +# groupby_obj._history.append_event("groupby", *args, **kwargs) +# groupby_obj.pre_aggregated = True +# return groupby_obj diff --git a/lux/core/old_frame.py b/lux/core/old_frame.py new file mode 100644 index 00000000..4d8da0e8 --- /dev/null +++ b/lux/core/old_frame.py @@ -0,0 +1,874 @@ +# Copyright 2019-2020 The Lux Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +from lux.core.series import LuxSeries +from lux.vis.Clause import Clause +from lux.vis.Vis import Vis +from lux.vis.VisList import VisList +from lux.history.history import History +from lux.utils.date_utils import is_datetime_series +from lux.utils.message import Message +from lux.utils.utils import check_import_lux_widget +from typing import Dict, Union, List, Callable + +# from lux.executor.Executor import * +import warnings +import traceback +import lux + + +class LuxDataFrame(pd.DataFrame): + """ + A subclass of pd.DataFrame that supports all dataframe operations while housing other variables and functions for generating visual recommendations. + """ + + # MUST register here for new properties!! + _metadata = [ + "_intent", + "_inferred_intent", + "_data_type", + "unique_values", + "cardinality", + "_rec_info", + "_min_max", + "_current_vis", + "_widget", + "_recommendation", + "_prev", + "_history", + "_saved_export", + "_sampled", + "_toggle_pandas_display", + "_message", + "_pandas_only", + "pre_aggregated", + "_type_override", + ] + + def __init__(self, *args, **kw): + self._history = History() + self._intent = [] + self._inferred_intent = [] + self._recommendation = {} + self._saved_export = None + self._current_vis = [] + self._prev = None + self._widget = None + super(LuxDataFrame, self).__init__(*args, **kw) + + self.table_name = "" + if lux.config.SQLconnection == "": + from lux.executor.PandasExecutor import PandasExecutor + + lux.config.executor = PandasExecutor() + else: + from lux.executor.SQLExecutor import SQLExecutor + + # lux.config.executor = SQLExecutor() + + self._sampled = None + self._approx_sample = None + self._toggle_pandas_display = True + self._message = Message() + self._pandas_only = False + # Metadata + self._data_type = {} + self.unique_values = None + self.cardinality = None + self._min_max = None + self.pre_aggregated = None + self._type_override = {} + warnings.formatwarning = lux.warning_format + + @property + def _constructor(self): + return LuxDataFrame + + @property + def _constructor_sliced(self): + def f(*args, **kwargs): + s = LuxSeries(*args, **kwargs) + for attr in self._metadata: # propagate metadata + s.__dict__[attr] = getattr(self, attr, None) + return s + + return f + + @property + def history(self): + return self._history + + @property + def data_type(self): + if not self._data_type: + self.maintain_metadata() + return self._data_type + + def compute_metadata(self) -> None: + """ + Compute dataset metadata and statistics + """ + if len(self) > 0: + if lux.config.executor.name != "SQLExecutor": + lux.config.executor.compute_stats(self) + lux.config.executor.compute_dataset_metadata(self) + self._infer_structure() + self._metadata_fresh = True + + def maintain_metadata(self): + """ + Maintain dataset metadata and statistics (Compute only if needed) + """ + is_sql_tbl = lux.config.executor.name != "PandasExecutor" + + if lux.config.SQLconnection != "" and is_sql_tbl: + from lux.executor.SQLExecutor import SQLExecutor + + # lux.config.executor = SQLExecutor() + + # Check that metadata has not yet been computed + if lux.config.lazy_maintain: + # Check that metadata has not yet been computed + if not hasattr(self, "_metadata_fresh") or not self._metadata_fresh: + # only compute metadata information if the dataframe is non-empty + self.compute_metadata() + else: + self.compute_metadata() + + def expire_recs(self) -> None: + """ + Expires and resets all recommendations + """ + if lux.config.lazy_maintain: + self._recs_fresh = False + self._recommendation = {} + self._widget = None + self._rec_info = None + self._sampled = None + + def expire_metadata(self) -> None: + """ + Expire all saved metadata to trigger a recomputation the next time the data is required. + """ + if lux.config.lazy_maintain: + self._metadata_fresh = False + self._data_type = None + self.unique_values = None + self.cardinality = None + self._min_max = None + self.pre_aggregated = None + + ##################### + ## Override Pandas ## + ##################### + def __getattr__(self, name): + ret_value = super(LuxDataFrame, self).__getattr__(name) + self.expire_metadata() + self.expire_recs() + return ret_value + + def _set_axis(self, axis, labels): + super(LuxDataFrame, self)._set_axis(axis, labels) + self.expire_metadata() + self.expire_recs() + + def _update_inplace(self, *args, **kwargs): + super(LuxDataFrame, self)._update_inplace(*args, **kwargs) + self.expire_metadata() + self.expire_recs() + + def _set_item(self, key, value): + super(LuxDataFrame, self)._set_item(key, value) + self.expire_metadata() + self.expire_recs() + + def _infer_structure(self): + # If the dataframe is very small and the index column is not a range index, then it is likely that this is an aggregated data + is_multi_index_flag = self.index.nlevels != 1 + not_int_index_flag = not pd.api.types.is_integer_dtype(self.index) + + is_sql_tbl = lux.config.executor.name != "PandasExecutor" + + small_df_flag = len(self) < 100 and is_sql_tbl + if self.pre_aggregated == None: + self.pre_aggregated = (is_multi_index_flag or not_int_index_flag) and small_df_flag + if "Number of Records" in self.columns: + self.pre_aggregated = True + self.pre_aggregated = "groupby" in [event.name for event in self.history] and not is_sql_tbl + + @property + def intent(self): + """ + Main function to set the intent of the dataframe. + The intent input goes through the parser, so that the string inputs are parsed into a lux.Clause object. + + Parameters + ---------- + intent : List[str,Clause] + intent list, can be a mix of string shorthand or a lux.Clause object + + Notes + ----- + :doc:`../guide/intent` + """ + return self._intent + + @intent.setter + def intent(self, intent_input: Union[List[Union[str, Clause]], Vis]): + is_list_input = isinstance(intent_input, list) + is_vis_input = isinstance(intent_input, Vis) + if not (is_list_input or is_vis_input): + raise TypeError( + "Input intent must be either a list (of strings or lux.Clause) or a lux.Vis object." + "\nSee more at: https://lux-api.readthedocs.io/en/latest/source/guide/intent.html" + ) + if is_list_input: + self.set_intent(intent_input) + elif is_vis_input: + self.set_intent_as_vis(intent_input) + + def clear_intent(self): + self.intent = [] + self.expire_recs() + + def set_intent(self, intent: List[Union[str, Clause]]): + self.expire_recs() + self._intent = intent + self._parse_validate_compile_intent() + + def _parse_validate_compile_intent(self): + self.maintain_metadata() + from lux.processor.Parser import Parser + from lux.processor.Validator import Validator + + self._intent = Parser.parse(self._intent) + Validator.validate_intent(self._intent, self) + self.maintain_metadata() + from lux.processor.Compiler import Compiler + + self.current_vis = Compiler.compile_intent(self, self._intent) + + def copy_intent(self): + # creates a true copy of the dataframe's intent + output = [] + for clause in self._intent: + temp_clause = clause.copy_clause() + output.append(temp_clause) + return output + + def set_intent_as_vis(self, vis: Vis): + """ + Set intent of the dataframe based on the intent of a Vis + + Parameters + ---------- + vis : Vis + Input Vis object + """ + self.expire_recs() + self._intent = vis._inferred_intent + self._parse_validate_compile_intent() + + def set_data_type(self, types: dict): + """ + Set the data type for a particular attribute in the dataframe + overriding the automatically-detected type inferred by Lux + + Parameters + ---------- + types: dict + Dictionary that maps attribute/column name to a specified Lux Type. + Possible options: "nominal", "quantitative", "id", and "temporal". + + Example + ---------- + df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/absenteeism.csv") + df.set_data_type({"ID":"id", + "Reason for absence":"nominal"}) + """ + if self._type_override == None: + self._type_override = types + else: + self._type_override = {**self._type_override, **types} + + if not self.data_type: + self.maintain_metadata() + + for attr in types: + if types[attr] not in ["nominal", "quantitative", "id", "temporal"]: + raise ValueError( + f'Invalid data type option specified for {attr}. Please use one of the following supported types: ["nominal", "quantitative", "id", "temporal"]' + ) + self.data_type[attr] = types[attr] + + self.expire_recs() + + def to_pandas(self): + import lux.core + + return lux.core.originalDF(self, copy=False) + + @property + def recommendation(self): + if self._recommendation is not None and self._recommendation == {}: + from lux.processor.Compiler import Compiler + + self.maintain_metadata() + self.current_vis = Compiler.compile_intent(self, self._intent) + self.maintain_recs() + return self._recommendation + + @recommendation.setter + def recommendation(self, recommendation: Dict): + self._recommendation = recommendation + + @property + def current_vis(self): + from lux.processor.Validator import Validator + + # _parse_validate_compile_intent does not call executor, + # we only attach data to current vis when user request current_vis + valid_current_vis = ( + self._current_vis is not None + and len(self._current_vis) > 0 + and self._current_vis[0].data is None + and self._current_vis[0].intent + ) + if valid_current_vis and Validator.validate_intent(self._current_vis[0].intent, self): + lux.config.executor.execute(self._current_vis, self) + return self._current_vis + + @current_vis.setter + def current_vis(self, current_vis: Dict): + self._current_vis = current_vis + + def _append_rec(self, rec_infolist, recommendations: Dict): + if recommendations["collection"] is not None and len(recommendations["collection"]) > 0: + rec_infolist.append(recommendations) + + def show_all_column_vis(self): + if len(self.columns) > 1 and len(self.columns) < 4 and self.intent == [] or self.intent is None: + vis = Vis(list(self.columns), self) + if vis.mark != "": + vis._all_column = True + self.current_vis = VisList([vis]) + + def maintain_recs(self, is_series="DataFrame"): + # `rec_df` is the dataframe to generate the recommendations on + # check to see if globally defined actions have been registered/removed + if lux.config.update_actions["flag"] == True: + self._recs_fresh = False + show_prev = False # flag indicating whether rec_df is showing previous df or current self + + if self._prev is not None: + rec_df = self._prev + rec_df._message = Message() + rec_df.maintain_metadata() # the prev dataframe may not have been printed before + last_event = self.history._events[-1].name + rec_df._message.add( + f"Lux is visualizing the previous version of the dataframe before you applied {last_event}." + ) + show_prev = True + else: + rec_df = self + rec_df._message = Message() + # Add warning message if there exist ID fields + if len(rec_df) == 0: + rec_df._message.add(f"Lux cannot operate on an empty {is_series}.") + elif len(rec_df) < 5 and not rec_df.pre_aggregated: + rec_df._message.add( + f"The {is_series} is too small to visualize. To generate visualizations in Lux, the {is_series} must contain at least 5 rows." + ) + elif self.index.nlevels >= 2 or self.columns.nlevels >= 2: + rec_df._message.add( + f"Lux does not currently support visualizations in a {is_series} " + f"with hierarchical indexes.\n" + f"Please convert the {is_series} into a flat " + f"table via pandas.DataFrame.reset_index." + ) + else: + id_fields_str = "" + inverted_data_type = lux.config.executor.invert_data_type(rec_df.data_type) + if len(inverted_data_type["id"]) > 0: + for id_field in inverted_data_type["id"]: + id_fields_str += f"{id_field}, " + id_fields_str = id_fields_str[:-2] + rec_df._message.add(f"{id_fields_str} is not visualized since it resembles an ID field.") + + rec_df._prev = None # reset _prev + + # If lazy, check that recs has not yet been computed + lazy_but_not_computed = lux.config.lazy_maintain and ( + not hasattr(rec_df, "_recs_fresh") or not rec_df._recs_fresh + ) + eager = not lux.config.lazy_maintain + + # Check that recs has not yet been computed + if lazy_but_not_computed or eager: + is_sql_tbl = lux.config.executor.name == "SQLExecutor" + rec_infolist = [] + from lux.action.row_group import row_group + from lux.action.column_group import column_group + + # TODO: Rewrite these as register action inside default actions + if rec_df.pre_aggregated: + if rec_df.columns.name is not None: + rec_df._append_rec(rec_infolist, row_group(rec_df)) + rec_df._append_rec(rec_infolist, column_group(rec_df)) + elif not (len(rec_df) < 5 and not rec_df.pre_aggregated and not is_sql_tbl) and not ( + self.index.nlevels >= 2 or self.columns.nlevels >= 2 + ): + from lux.action.custom import custom_actions + + # generate vis from globally registered actions and append to dataframe + custom_action_collection = custom_actions(rec_df) + for rec in custom_action_collection: + rec_df._append_rec(rec_infolist, rec) + lux.config.update_actions["flag"] = False + + # Store _rec_info into a more user-friendly dictionary form + rec_df._recommendation = {} + for rec_info in rec_infolist: + action_type = rec_info["action"] + vlist = rec_info["collection"] + if len(vlist) > 0: + rec_df._recommendation[action_type] = vlist + rec_df._rec_info = rec_infolist + rec_df.show_all_column_vis() + if lux.config.render_widget: + self._widget = rec_df.render_widget() + # re-render widget for the current dataframe if previous rec is not recomputed + elif show_prev: + rec_df.show_all_column_vis() + if lux.config.render_widget: + self._widget = rec_df.render_widget() + self._recs_fresh = True + + ####################################################### + ############## LuxWidget Result Display ############### + ####################################################### + @property + def widget(self): + if self._widget: + return self._widget + + @property + def exported(self) -> Union[Dict[str, VisList], VisList]: + """ + Get selected visualizations as exported Vis List + + Notes + ----- + Convert the _selectedVisIdxs dictionary into a programmable VisList + Example _selectedVisIdxs : + + {'Correlation': [0, 2], 'Occurrence': [1]} + + indicating the 0th and 2nd vis from the `Correlation` tab is selected, and the 1st vis from the `Occurrence` tab is selected. + + Returns + ------- + Union[Dict[str,VisList], VisList] + When there are no exported vis, return empty list -> [] + When all the exported vis is from the same tab, return a VisList of selected visualizations. -> VisList(v1, v2...) + When the exported vis is from the different tabs, return a dictionary with the action name as key and selected visualizations in the VisList. -> {"Enhance": VisList(v1, v2...), "Filter": VisList(v5, v7...), ..} + """ + if self.widget is None: + warnings.warn( + "\nNo widget attached to the dataframe." + "Please assign dataframe to an output variable.\n" + "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", + stacklevel=2, + ) + return [] + exported_vis_lst = self._widget._selectedVisIdxs + exported_vis = [] + if exported_vis_lst == {}: + if self._saved_export: + return self._saved_export + warnings.warn( + "\nNo visualization selected to export.\n" + "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", + stacklevel=2, + ) + return [] + if len(exported_vis_lst) == 1 and "currentVis" in exported_vis_lst: + return self.current_vis + elif len(exported_vis_lst) > 1: + exported_vis = {} + if "currentVis" in exported_vis_lst: + exported_vis["Current Vis"] = self.current_vis + for export_action in exported_vis_lst: + if export_action != "currentVis": + exported_vis[export_action] = VisList( + list( + map( + self._recommendation[export_action].__getitem__, + exported_vis_lst[export_action], + ) + ) + ) + return exported_vis + elif len(exported_vis_lst) == 1 and ("currentVis" not in exported_vis_lst): + export_action = list(exported_vis_lst.keys())[0] + exported_vis = VisList( + list( + map( + self._recommendation[export_action].__getitem__, + exported_vis_lst[export_action], + ) + ) + ) + self._saved_export = exported_vis + return exported_vis + else: + warnings.warn( + "\nNo visualization selected to export.\n" + "See more: https://lux-api.readthedocs.io/en/latest/source/guide/FAQ.html#troubleshooting-tips", + stacklevel=2, + ) + return [] + + def remove_deleted_recs(self, change): + for action in self._widget.deletedIndices: + deletedSoFar = 0 + for index in self._widget.deletedIndices[action]: + self._recommendation[action].remove_index(index - deletedSoFar) + deletedSoFar += 1 + + def set_intent_on_click(self, change): + from IPython.display import display, clear_output + from lux.processor.Compiler import Compiler + + intent_action = list(self._widget.selectedIntentIndex.keys())[0] + vis = self._recommendation[intent_action][self._widget.selectedIntentIndex[intent_action][0]] + self.set_intent_as_vis(vis) + + self.maintain_metadata() + self.current_vis = Compiler.compile_intent(self, self._intent) + self.maintain_recs() + + with self.output: + clear_output() + display(self._widget) + + self._widget.observe(self.remove_deleted_recs, names="deletedIndices") + self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") + + def _ipython_display_(self): + from IPython.display import display + from IPython.display import clear_output + import ipywidgets as widgets + + try: + if self._pandas_only: + display(self.display_pandas()) + self._pandas_only = False + else: + if not self.index.nlevels >= 2 or self.columns.nlevels >= 2: + self.maintain_metadata() + + if self._intent != [] and (not hasattr(self, "_compiled") or not self._compiled): + from lux.processor.Compiler import Compiler + + self.current_vis = Compiler.compile_intent(self, self._intent) + + if lux.config.default_display == "lux": + self._toggle_pandas_display = False + else: + self._toggle_pandas_display = True + + # df_to_display.maintain_recs() # compute the recommendations (TODO: This can be rendered in another thread in the background to populate self._widget) + self.maintain_recs() + + # Observers(callback_function, listen_to_this_variable) + self._widget.observe(self.remove_deleted_recs, names="deletedIndices") + self._widget.observe(self.set_intent_on_click, names="selectedIntentIndex") + + button = widgets.Button( + description="Toggle Pandas/Lux", + layout=widgets.Layout(width="140px", top="5px"), + ) + self.output = widgets.Output() + display(button, self.output) + + def on_button_clicked(b): + with self.output: + if b: + self._toggle_pandas_display = not self._toggle_pandas_display + clear_output() + if self._toggle_pandas_display: + display(self.display_pandas()) + else: + # b.layout.display = "none" + display(self._widget) + # b.layout.display = "inline-block" + + button.on_click(on_button_clicked) + on_button_clicked(None) + + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + if lux.config.pandas_fallback: + warnings.warn( + "\nUnexpected error in rendering Lux widget and recommendations. " + "Falling back to Pandas display.\n" + "Please report the following issue on Github: https://github.com/lux-org/lux/issues \n", + stacklevel=2, + ) + warnings.warn(traceback.format_exc()) + display(self.display_pandas()) + else: + raise + + def display_pandas(self): + return self.to_pandas() + + def render_widget(self, renderer: str = "altair", input_current_vis=""): + """ + Generate a LuxWidget based on the LuxDataFrame + + Structure of widgetJSON: + + { + + 'current_vis': {}, + 'recommendation': [ + + { + + 'action': 'Correlation', + 'description': "some description", + 'vspec': [ + + {Vega-Lite spec for vis 1}, + {Vega-Lite spec for vis 2}, + ... + + ] + + }, + ... repeat for other actions + + ] + + } + + Parameters + ---------- + renderer : str, optional + Choice of visualization rendering library, by default "altair" + input_current_vis : lux.LuxDataFrame, optional + User-specified current vis to override default Current Vis, by default + + """ + check_import_lux_widget() + import luxwidget + + widgetJSON = self.to_JSON(self._rec_info, input_current_vis=input_current_vis) + return luxwidget.LuxWidget( + currentVis=widgetJSON["current_vis"], + recommendations=widgetJSON["recommendation"], + intent=LuxDataFrame.intent_to_string(self._intent), + message=self._message.to_html(), + config={"plottingScale": lux.config.plotting_scale}, + ) + + @staticmethod + def intent_to_JSON(intent): + from lux.utils import utils + + filter_specs = utils.get_filter_specs(intent) + attrs_specs = utils.get_attrs_specs(intent) + + intent = {} + intent["attributes"] = [clause.attribute for clause in attrs_specs] + intent["filters"] = [clause.attribute for clause in filter_specs] + return intent + + @staticmethod + def intent_to_string(intent): + if intent: + return ", ".join([clause.to_string() for clause in intent]) + else: + return "" + + def to_JSON(self, rec_infolist, input_current_vis=""): + widget_spec = {} + if self.current_vis: + lux.config.executor.execute(self.current_vis, self) + widget_spec["current_vis"] = LuxDataFrame.current_vis_to_JSON( + self.current_vis, input_current_vis + ) + else: + widget_spec["current_vis"] = {} + widget_spec["recommendation"] = [] + + # Recommended Collection + recCollection = LuxDataFrame.rec_to_JSON(rec_infolist) + widget_spec["recommendation"].extend(recCollection) + return widget_spec + + @staticmethod + def current_vis_to_JSON(vlist, input_current_vis=""): + current_vis_spec = {} + numVC = len(vlist) # number of visualizations in the vis list + if numVC == 1: + current_vis_spec = vlist[0].to_code(language=lux.config.plotting_backend, prettyOutput=False) + elif numVC > 1: + pass + if vlist[0]._all_column: + current_vis_spec["allcols"] = True + else: + current_vis_spec["allcols"] = False + return current_vis_spec + + @staticmethod + def rec_to_JSON(recs): + rec_lst = [] + import copy + + rec_copy = copy.deepcopy(recs) + for idx, rec in enumerate(rec_copy): + if len(rec["collection"]) > 0: + rec["vspec"] = [] + for vis in rec["collection"]: + chart = vis.to_code(language=lux.config.plotting_backend, prettyOutput=False) + rec["vspec"].append(chart) + rec_lst.append(rec) + # delete since not JSON serializable + del rec_lst[idx]["collection"] + return rec_lst + + def save_as_html(self, filename: str = "export.html", output=False): + """ + Save dataframe widget as static HTML file + + Parameters + ---------- + filename : str + Filename for the output HTML file + """ + + if self.widget is None: + self.maintain_metadata() + self.maintain_recs() + + from ipywidgets.embed import embed_data + + data = embed_data(views=[self.widget]) + + import json + + manager_state = json.dumps(data["manager_state"]) + widget_view = json.dumps(data["view_specs"][0]) + + # Separate out header since CSS file conflict with {} notation in Python format strings + header = """ + + + Lux Widget + + + + + + + + + + + """ + html_template = """ + + {header} + + + + + + + + + + + """ + + manager_state = json.dumps(data["manager_state"]) + widget_view = json.dumps(data["view_specs"][0]) + rendered_template = html_template.format( + header=header, manager_state=manager_state, widget_view=widget_view + ) + if output: + return rendered_template + else: + with open(filename, "w") as fp: + fp.write(rendered_template) + print(f"Saved HTML to {filename}") + + # Overridden Pandas Functions + def head(self, n: int = 5): + ret_val = super(LuxDataFrame, self).head(n) + ret_val._prev = self + ret_val._history.append_event("head", n=5) + return ret_val + + def tail(self, n: int = 5): + ret_val = super(LuxDataFrame, self).tail(n) + ret_val._prev = self + ret_val._history.append_event("tail", n=5) + return ret_val + + def groupby(self, *args, **kwargs): + history_flag = False + if "history" not in kwargs or ("history" in kwargs and kwargs["history"]): + history_flag = True + if "history" in kwargs: + del kwargs["history"] + groupby_obj = super(LuxDataFrame, self).groupby(*args, **kwargs) + for attr in self._metadata: + groupby_obj.__dict__[attr] = getattr(self, attr, None) + if history_flag: + groupby_obj._history = groupby_obj._history.copy() + groupby_obj._history.append_event("groupby", *args, **kwargs) + groupby_obj.pre_aggregated = True + return groupby_obj diff --git a/test_nb/refactor test.ipynb b/test_nb/refactor test.ipynb new file mode 100644 index 00000000..11ee0981 --- /dev/null +++ b/test_nb/refactor test.ipynb @@ -0,0 +1,152 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "baking-asian", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/dorislee/Desktop/Research/lux/dorisjlee_fork\n" + ] + }, + { + "data": { + "text/plain": [ + "['~/Desktop/Research/lux/Untitled Folder']" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pushd ../dorisjlee_fork/" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "oriental-security", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from lux.core.frame import *" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "thick-hammer", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "valued-monroe", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "df = DataFrame(dict(x=np.arange(100)))\n", + "df[\"y\"] = df.x ** 2 + 1 + 200 * np.random.normal(size=df.x.shape)\n", + "df[\"z\"] = (100 - df.x) ** 2 + 1 + 200 * np.random.normal(size=df.x.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "moving-malpractice", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/dorislee/Desktop/Research/lux/dorisjlee_fork/lux/core/frame.py:86: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access\n", + " self.recommendation={'Correlation': []}\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "blind-typing", + "metadata": {}, + "outputs": [], + "source": [ + "df.intent=[\"y\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "banned-median", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}