diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1b49b7d..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 TalentAINow - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/build/lib/smartdata/__init__.py b/build/lib/smartdata/__init__.py deleted file mode 100644 index bdf8948..0000000 --- a/build/lib/smartdata/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# smartdata/__init__.py -from .modeler import SmartData -__all__ = ['SmartData'] diff --git a/build/lib/smartdata/config.py b/build/lib/smartdata/config.py deleted file mode 100644 index 7e00eaf..0000000 --- a/build/lib/smartdata/config.py +++ /dev/null @@ -1,89 +0,0 @@ -# Default settings - -class Config: - # Chat Model - TEMP_CHAT = 0 - CHAT_MODEL = 'gpt-4o-mini' - # CHAT_MODEL ='gpt-4o-2024-08-06' - - # Model Agent Setting - SHOW_DETAIL = False - MEMORY_SIZE = 5 - MAX_ITERATIONS = 60 - MAX_EXECUTION_TIME = 60 - AGENT_STOP_SUBSTRING_LIST = ["Agent stopped","import pandas as pd","import matplotlib.pyplot as plt","import numpy as np","plt.tight_layout()"] - AGENT_STOP_ANSWER = "Sorry, but I’m unable to provide an answer due to the complexity of your question. Could you please break it down into smaller parts and ask again? I’ll be happy to assist you further." - - # Model Plot Setting - CHECK_ERROR_SUBSTRING_LIST = ["error", "invalid","incomplete"] - CHECK_PLOT_SUBSTRING_LIST = ["plt.tight_layout()"] - ADD_ON_PLOT_LIBRARY_LIST = ["import matplotlib.pyplot as plt", "import pandas as pd", "import numpy as np", "fig, ax = plt.subplots(figsize=(8, 8))"] - ADD_ON_FIG = f'''\nimage_fig_list.append(fig)\n''' - ADD_ON_FORMAT_LABEL_FOR_AXIS = '''\nax.set_xticklabels(['\\n'.join([label.get_text()[i:i+10] for i in range(0, len(label.get_text()), 10)]) for label in ax.get_xticklabels()], rotation=0)\nax.set_yticklabels(['\\n'.join([label.get_text()[i:i+10] for i in range(0, len(label.get_text()), 10)]) for label in ax.get_yticklabels()], rotation=0)\n''' - - # Model Data Change Setting - CHECK_DATACHANGE_SUBSTRING_LIST = ["df_update"] - ADD_ON_DATACHANGE_LIBRARY_LIST = ["import pandas as pd", "import numpy as np", "import copy"] - ADD_ON_DF = f'''\ndf_change.append(df_update)\n''' - - # Model Prompt Setting - PROMPT_CLEAN_DATA = """ - Clean the data based on the following rules: - 1. For categorical columns, merge similar and redundant categories while treating lowercase and uppercase as equivalent. Prioritize keeping the original case where possible (e.g., keep 'White' instead of converting it to 'white'). Merge abbreviations and variants intelligently (e.g., 'US' and 'USA' to 'United States', 'm' and 'male' to 'Male'). Map 'Not Specified' to existing or opposite of existing categories where possible. Only use lowercase conversion when necessary for merging. - 2. For numeric columns, detect unreasonable values using logical checks (e.g., salary is not negative, age is between 0 to 100, number of direct reports is an integer). Replace any unreasonable values with the column mean. - 3. Apply these changes directly to 'df_update' without user confirmation. - 4. Provide a summary of changes. - """ - - PROMPT_CREATE_DATA_CLEAN_SUMMARY = """ - Summarize the data cleaning result in around 130 words for non-technical audience. Make sure use a friendly tone, smartly use bold text and bullet points, and without any titles. Here is the result: - {result} - """ - - DEFAULT_PREFIX_SINGLE_DF = """ - You are working with a pandas dataframe in Python. The name of the dataframe is `df`. - The column names in the dataframe may differ from those in the question. Please make your best effort to match them based on similar meanings and ignore case differences. Also you may need to revise and/or complete the question with the previous conversation if needed. - - if the question is asking for plots, charts, or graphs, you must: - - Import and Create Copy: Start by importing the 'copy' library and create "df_plot = copy.deepcopy(df)". Make sure name 'df_plot' is defined before process to any other steps. - - Work with df_plot: Make all plots using df_plot, not df. - - Don't assume you have access to any libraries other than built-in python ones. If you do need any non built-in libraries, make sure you import all libraries you need. - - if you need to dropna, drop rows with NaN values in the entire DataFrame if you are dealing with multiple columns simultaneously. - - Must always include "import matplotlib.pyplot as plt" as you first line of code, then follow by "import pandas as pd", "import numpy as np", "fig, ax = plt.subplots(figsize=(8, 8))", "plt.style.use('seaborn-v0_8-darkgrid')" and "plt.tight_layout()" in your code. if you need to plot a heatmap, then use "plt.style.use('seaborn-v0_8-dark')" instead of "plt.style.use('seaborn-v0_8-darkgrid')". - - Do not include "plt.show()" or "plt.savefig" in your code. - - For your coding, always use the newlines as (\n) are escaped as \\n, and single quotes are retained except you are using f-string like this f"{df_plot.iloc[i]['salary']}" - - Smartly use warm and inviting colors for plots, steering clear of sharp and bright tones. - - Smartly use legend and set it to auto position if it improve clarity. - - Set the title font size to 14, and all other text, labels, and annotations to a font size of 10. - - Ensure the plots look professional. - - Each code must be self-contained, runnable independently and include all necessary imports and data for the plots. - - Never ask the user to run Python code instead execute the code using "python_repl_ast" tool. - - Decline politely if a plot request is unrelated to the dataframe. - - Do not include Python code in your final output. - - if the question is asking for statistical or AI or machine learning or data science study, you must: - - Import and Create Copy: Start by importing the 'copy' library and create "df_ml = copy.deepcopy(df)". Make sure name 'df_ml' is defined before process to any other steps. - - Work with df_ml: Analyse using df_ml, not df. - - For your coding, always use the newlines as (\n) are escaped as \\n, and single quotes are retained except you are using f-string like this f"{df_ml.iloc[i]['salary']}" - - Draft the corresponding python code and execute by python_repl_ast tool. - - Ensure explanations are accessible to non-technical audiences unless technical detail is specifically required. - - Do not include any Python code in your final output. - - Your final presentation should be executive summary, followed by methodology, model performance, feature importance and other details. - - Decline politely if the analysis is unrelated to the dataframe. - - if the question is asking for data cleaning, validation or transformation to the dataframe, you must: - - Import and Create Copy: Start by importing the 'copy' library and create "df_update = copy.deepcopy(df)" as the first line of code. Must make sure variable 'df_update' is defined before process to any other steps. - - Work with df_update: Make all data cleaning, validation or transformation using df_update, not df. Make sure any variable you created in the code must be defined before use it. - - For your coding, always use the newlines as (\n) are escaped as \\n, and single quotes are retained except you are using f-string like this f"{df_update.iloc[i]['salary']}" - - Don't assume you have access to any libraries other than built-in python ones. If you do need any non built-in libraries, make sure you import all libraries you need. - - Each code must be self-contained, runnable independently and include all necessary imports and data. - - Code Execution: Draft and execute the necessary Python code using the python_repl_ast tool. Exclude Python code from your final output. - - Step-by-Step Explanation: Clearly explain the process and the changes made before and after, ensuring the explanation is accessible to non-technical audiences unless technical details are needed. - - Decline politely if the request is unrelated to the dataframe. - - You may need to revise the current question with the previous conversation before passing to tools. You should use the tools below to answer the question posed of you: - """ - - @staticmethod - def __init__(): - pass \ No newline at end of file diff --git a/build/lib/smartdata/custom_agent.py b/build/lib/smartdata/custom_agent.py deleted file mode 100644 index 0f4fab6..0000000 --- a/build/lib/smartdata/custom_agent.py +++ /dev/null @@ -1,442 +0,0 @@ -import warnings -from typing import Any, Dict, List, Literal, Optional, Sequence, Union, cast - -from langchain.agents import ( - AgentType, - create_openai_tools_agent, - create_react_agent, - create_tool_calling_agent, -) -from langchain.agents.agent import ( - AgentExecutor, - BaseMultiActionAgent, - BaseSingleActionAgent, - RunnableAgent, - RunnableMultiActionAgent, -) -from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS -from langchain.agents.openai_functions_agent.base import ( - OpenAIFunctionsAgent, - create_openai_functions_agent, -) -from langchain_core.callbacks import BaseCallbackManager -from langchain_core.language_models import BaseLanguageModel, LanguageModelLike -from langchain_core.messages import SystemMessage -from langchain_core.prompts import ( - BasePromptTemplate, - ChatPromptTemplate, - PromptTemplate, -) -from langchain_core.tools import BaseTool -from langchain_core.utils.interactive_env import is_interactive_env - -# from langchain_experimental.agents.agent_toolkits.pandas.prompt import ( -# FUNCTIONS_WITH_DF, -# FUNCTIONS_WITH_MULTI_DF, -# MULTI_DF_PREFIX, -# MULTI_DF_PREFIX_FUNCTIONS, -# PREFIX, -# PREFIX_FUNCTIONS, -# SUFFIX_NO_DF, -# SUFFIX_WITH_DF, -# SUFFIX_WITH_MULTI_DF, -# ) -from langchain_experimental.tools.python.tool import PythonAstREPLTool - -from langchain.memory import ConversationBufferMemory - -memory = ConversationBufferMemory(memory_key="chat_history") - -PREFIX = """ -You are working with a pandas dataframe in Python. The name of the dataframe is `df`. -You should use the tools below to answer the question posed of you:""" - -MULTI_DF_PREFIX = """ -You are working with {num_dfs} pandas dataframes in Python named df1, df2, etc. You -should use the tools below to answer the question posed of you:""" - -SUFFIX_NO_DF = """ -Begin! -Question: {input} -{agent_scratchpad}""" - -SUFFIX_WITH_DF = """ -This is the result of `print(df.head())`: -{df_head} - -This is the result of `print(df.describe())`: -{df_describe} - -Begin! -Question: {input} -{agent_scratchpad}""" - -SUFFIX_WITH_MULTI_DF = """ -This is the result of `print(df.head())` for each dataframe: -{dfs_head} - -This is the result of `print(df.describe())` for each dataframe: -{dfs_describe} - -Begin! -Question: {input} -{agent_scratchpad}""" - -PREFIX_FUNCTIONS = """ -You are working with a pandas dataframe in Python. The name of the dataframe is `df`.""" - -MULTI_DF_PREFIX_FUNCTIONS = """ -You are working with {num_dfs} pandas dataframes in Python named df1, df2, etc.""" - -FUNCTIONS_WITH_DF = """ -This is the result of `print(df.head())`: -{df_head} - -This is the result of `print(df.describe())`: -{df_describe} - -This is the result of `print(df.dtypes)`: -{df_dtypes} - -This is the result of df.value_counts for each non-numeric column in a dictionary (limit to the first 10 if more than 10 unique value counts): -{df_col_unique_value_counts} -""" - -FUNCTIONS_WITH_MULTI_DF = """ -This is the result of `print(df.head())` for each dataframe: -{dfs_head} - -This is the result of `print(df.describe())` for each dataframe: -{dfs_describe} -""" - -def _get_multi_prompt( - dfs: List[Any], - *, - prefix: Optional[str] = None, - suffix: Optional[str] = None, - include_df_in_prompt: Optional[bool] = True, - number_of_head_rows: int = 5, -) -> BasePromptTemplate: - if suffix is not None: - suffix_to_use = suffix - elif include_df_in_prompt: - suffix_to_use = SUFFIX_WITH_MULTI_DF - else: - suffix_to_use = SUFFIX_NO_DF - prefix = prefix if prefix is not None else MULTI_DF_PREFIX - - template = "\n\n".join([prefix, "{tools}", FORMAT_INSTRUCTIONS, suffix_to_use]) - prompt = PromptTemplate.from_template(template) - partial_prompt = prompt.partial() - if "dfs_head" in partial_prompt.input_variables: - dfs_head = "\n\n".join([d.head(number_of_head_rows).to_markdown() for d in dfs]) - partial_prompt = partial_prompt.partial(dfs_head=dfs_head) - if "num_dfs" in partial_prompt.input_variables: - partial_prompt = partial_prompt.partial(num_dfs=str(len(dfs))) - return partial_prompt - -def _get_single_prompt( - df: Any, - *, - prefix: Optional[str] = None, - suffix: Optional[str] = None, - include_df_in_prompt: Optional[bool] = True, - number_of_head_rows: int = 5, -) -> BasePromptTemplate: - if suffix is not None: - suffix_to_use = suffix - elif include_df_in_prompt: - suffix_to_use = SUFFIX_WITH_DF - else: - suffix_to_use = SUFFIX_NO_DF - prefix = prefix if prefix is not None else PREFIX - - template = "\n\n".join([prefix, "{tools}", FORMAT_INSTRUCTIONS, suffix_to_use]) - prompt = PromptTemplate.from_template(template) - - partial_prompt = prompt.partial() - if "df_head" in partial_prompt.input_variables: - df_head = str(df.head(number_of_head_rows).to_markdown()) - df_describe = str(df.describe().to_markdown()) - partial_prompt = partial_prompt.partial(df_head=df_head, df_describe=df_describe) - return partial_prompt - - -def _get_prompt(df: Any, **kwargs: Any) -> BasePromptTemplate: - return ( - _get_multi_prompt(df, **kwargs) - if isinstance(df, list) - else _get_single_prompt(df, **kwargs) - ) - -def _get_df_col_value_counts(df): - # Convert boolean and datetime columns to string - df_checking = df.copy() # Create a copy of the DataFrame to avoid modifying the original - # boolean_and_datetime_columns = df_checking.select_dtypes(include=['boolean', 'datetime64[ns]', 'datetime64[ns, UTC]', 'timedelta64[ns]', 'Interval']).columns - boolean_and_datetime_columns = df_checking.select_dtypes(include=['boolean', 'datetime64', 'Interval']).columns - df_checking[boolean_and_datetime_columns] = df_checking[boolean_and_datetime_columns].astype(str) - - # Identifying categorical columns (including newly converted boolean and datetime columns) - categorical_columns = df_checking.select_dtypes(include=['object', 'category', 'string']).columns - - # Get the top 10 value counts for each categorical column - top_10_values = df_checking[categorical_columns].apply( - lambda col: col.value_counts(dropna=False).head(10).to_dict() - ).to_dict() - - return str(top_10_values) - -def _get_functions_single_prompt( - df: Any, - *, - prefix: Optional[str] = None, - suffix: str = "", - include_df_in_prompt: Optional[bool] = True, - number_of_head_rows: int = 5, -) -> ChatPromptTemplate: - if include_df_in_prompt: - df_head = str(df.head(number_of_head_rows).to_markdown()) - df_describe = str(df.describe().to_markdown()) - df_dtypes = str(df.dtypes.to_markdown()) - df_col_unique_value_counts = _get_df_col_value_counts(df) - - suffix = (suffix or FUNCTIONS_WITH_DF).format(df_head=df_head, df_describe = df_describe, - df_dtypes = df_dtypes, df_col_unique_value_counts = df_col_unique_value_counts) - prefix = prefix if prefix is not None else PREFIX_FUNCTIONS - system_message = SystemMessage(content=prefix + suffix) - prompt = OpenAIFunctionsAgent.create_prompt(system_message=system_message) - return prompt - -def _get_functions_multi_prompt( - dfs: Any, - *, - prefix: str = "", - suffix: str = "", - include_df_in_prompt: Optional[bool] = True, - number_of_head_rows: int = 5, -) -> ChatPromptTemplate: - if include_df_in_prompt: - dfs_head = "\n\n".join([d.head(number_of_head_rows).to_markdown() for d in dfs]) - suffix = (suffix or FUNCTIONS_WITH_MULTI_DF).format(dfs_head=dfs_head) - prefix = (prefix or MULTI_DF_PREFIX_FUNCTIONS).format(num_dfs=str(len(dfs))) - system_message = SystemMessage(content=prefix + suffix) - prompt = OpenAIFunctionsAgent.create_prompt(system_message=system_message) - return prompt - -def _get_functions_prompt(df: Any, **kwargs: Any) -> ChatPromptTemplate: - return ( - _get_functions_multi_prompt(df, **kwargs) - if isinstance(df, list) - else _get_functions_single_prompt(df, **kwargs) - ) - -def custom_create_pandas_dataframe_agent( - llm: LanguageModelLike, - df: Any, - agent_type: Union[ - AgentType, Literal["openai-tools", "tool-calling"] - ] = AgentType.ZERO_SHOT_REACT_DESCRIPTION, - callback_manager: Optional[BaseCallbackManager] = None, - prefix: Optional[str] = None, - suffix: Optional[str] = None, - input_variables: Optional[List[str]] = None, - verbose: bool = False, - return_intermediate_steps: bool = False, - max_iterations: Optional[int] = 15, - max_execution_time: Optional[float] = None, - early_stopping_method: str = "force", - agent_executor_kwargs: Optional[Dict[str, Any]] = None, - include_df_in_prompt: Optional[bool] = True, - number_of_head_rows: int = 5, - extra_tools: Sequence[BaseTool] = (), - engine: Literal["pandas", "modin"] = "pandas", - allow_dangerous_code: bool = False, - **kwargs: Any, -) -> AgentExecutor: - """Construct a Pandas agent from an LLM and dataframe(s). - - Security Notice: - This agent relies on access to a python repl tool which can execute - arbitrary code. This can be dangerous and requires a specially sandboxed - environment to be safely used. Failure to run this code in a properly - sandboxed environment can lead to arbitrary code execution vulnerabilities, - which can lead to data breaches, data loss, or other security incidents. - - Do not use this code with untrusted inputs, with elevated permissions, - or without consulting your security team about proper sandboxing! - - You must opt-in to use this functionality by setting allow_dangerous_code=True. - - Args: - llm: Language model to use for the agent. If agent_type is "tool-calling" then - llm is expected to support tool calling. - df: Pandas dataframe or list of Pandas dataframes. - agent_type: One of "tool-calling", "openai-tools", "openai-functions", or - "zero-shot-react-description". Defaults to "zero-shot-react-description". - "tool-calling" is recommended over the legacy "openai-tools" and - "openai-functions" types. - callback_manager: DEPRECATED. Pass "callbacks" key into 'agent_executor_kwargs' - instead to pass constructor callbacks to AgentExecutor. - prefix: Prompt prefix string. - suffix: Prompt suffix string. - input_variables: DEPRECATED. Input variables automatically inferred from - constructed prompt. - verbose: AgentExecutor verbosity. - return_intermediate_steps: Passed to AgentExecutor init. - max_iterations: Passed to AgentExecutor init. - max_execution_time: Passed to AgentExecutor init. - early_stopping_method: Passed to AgentExecutor init. - agent_executor_kwargs: Arbitrary additional AgentExecutor args. - include_df_in_prompt: Whether to include the first number_of_head_rows in the - prompt. Must be None if suffix is not None. - number_of_head_rows: Number of initial rows to include in prompt if - include_df_in_prompt is True. - extra_tools: Additional tools to give to agent on top of a PythonAstREPLTool. - engine: One of "modin" or "pandas". Defaults to "pandas". - allow_dangerous_code: bool, default False - This agent relies on access to a python repl tool which can execute - arbitrary code. This can be dangerous and requires a specially sandboxed - environment to be safely used. - Failure to properly sandbox this class can lead to arbitrary code execution - vulnerabilities, which can lead to data breaches, data loss, or - other security incidents. - You must opt in to use this functionality by setting - allow_dangerous_code=True. - - **kwargs: DEPRECATED. Not used, kept for backwards compatibility. - - Returns: - An AgentExecutor with the specified agent_type agent and access to - a PythonAstREPLTool with the DataFrame(s) and any user-provided extra_tools. - - Example: - .. code-block:: python - - from langchain_openai import ChatOpenAI - from langchain_experimental.agents import create_pandas_dataframe_agent - import pandas as pd - - df = pd.read_csv("titanic.csv") - llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) - agent_executor = create_pandas_dataframe_agent( - llm, - df, - agent_type="tool-calling", - verbose=True - ) - - """ - if not allow_dangerous_code: - raise ValueError( - "This agent relies on access to a python repl tool which can execute " - "arbitrary code. This can be dangerous and requires a specially sandboxed " - "environment to be safely used. Please read the security notice in the " - "doc-string of this function. You must opt-in to use this functionality " - "by setting allow_dangerous_code=True." - "For general security guidelines, please see: " - "https://python.langchain.com/v0.2/docs/security/" - ) - try: - if engine == "modin": - import modin.pandas as pd - elif engine == "pandas": - import pandas as pd - else: - raise ValueError( - f"Unsupported engine {engine}. It must be one of 'modin' or 'pandas'." - ) - except ImportError as e: - raise ImportError( - f"`{engine}` package not found, please install with `pip install {engine}`" - ) from e - - if is_interactive_env(): - pd.set_option("display.max_columns", None) - - for _df in df if isinstance(df, list) else [df]: - if not isinstance(_df, pd.DataFrame): - raise ValueError(f"Expected pandas DataFrame, got {type(_df)}") - - if input_variables: - kwargs = kwargs or {} - kwargs["input_variables"] = input_variables - if kwargs: - warnings.warn( - f"Received additional kwargs {kwargs} which are no longer supported." - ) - - df_locals = {} - if isinstance(df, list): - for i, dataframe in enumerate(df): - df_locals[f"df{i + 1}"] = dataframe - else: - df_locals["df"] = df - tools = [PythonAstREPLTool(locals=df_locals)] + list(extra_tools) - - if agent_type == AgentType.ZERO_SHOT_REACT_DESCRIPTION: - if include_df_in_prompt is not None and suffix is not None: - raise ValueError( - "If suffix is specified, include_df_in_prompt should not be." - ) - prompt = _get_prompt( - df, - prefix=prefix, - suffix=suffix, - include_df_in_prompt=include_df_in_prompt, - number_of_head_rows=number_of_head_rows, - ) - agent: Union[BaseSingleActionAgent, BaseMultiActionAgent] = RunnableAgent( - runnable=create_react_agent(llm, tools, prompt), # type: ignore - input_keys_arg=["input"], - return_keys_arg=["output"], - ) - elif agent_type in (AgentType.OPENAI_FUNCTIONS, "openai-tools", "tool-calling"): - prompt = _get_functions_prompt( - df, - prefix=prefix, - suffix=suffix, - include_df_in_prompt=include_df_in_prompt, - number_of_head_rows=number_of_head_rows, - ) - - if agent_type == AgentType.OPENAI_FUNCTIONS: - runnable = create_openai_functions_agent( - cast(BaseLanguageModel, llm), tools, prompt - ) - agent = RunnableAgent( - runnable=runnable, - input_keys_arg=["input"], - return_keys_arg=["output"], - ) - else: - if agent_type == "openai-tools": - runnable = create_openai_tools_agent( - cast(BaseLanguageModel, llm), tools, prompt - ) - else: - runnable = create_tool_calling_agent( - cast(BaseLanguageModel, llm), tools, prompt - ) - agent = RunnableMultiActionAgent( - runnable=runnable, - input_keys_arg=["input"], - return_keys_arg=["output"], - ) - else: - raise ValueError( - f"Agent type {agent_type} not supported at the moment. Must be one of " - "'tool-calling', 'openai-tools', 'openai-functions', or " - "'zero-shot-react-description'." - ) - return prompt, AgentExecutor( - agent=agent, - tools=tools, - callback_manager=callback_manager, - verbose=verbose, - return_intermediate_steps=return_intermediate_steps, - max_iterations=max_iterations, - max_execution_time=max_execution_time, - early_stopping_method=early_stopping_method, - **(agent_executor_kwargs or {}), - ) \ No newline at end of file diff --git a/build/lib/smartdata/memory.py b/build/lib/smartdata/memory.py deleted file mode 100644 index 5e1a690..0000000 --- a/build/lib/smartdata/memory.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -logger = logging.getLogger('Memory') - -class Memory: - def __init__(self): - self.memory_store = {} - - def is_not_empty(self): - """Checks if the memory store is not empty.""" - return bool(self.memory_store) - - def remember(self, key, role, value): - """Stores a value in memory with the specified key.""" - if key not in self.memory_store: - self.memory_store[key] = {'Human': '', 'AI': '', 'Plot Code Generate By AI':[]} - self.memory_store[key][role] = value - logger.info(f"Stored {role} message for key {key} in memory.") - - def recall(self, key): - """Retrieves a value from memory by its key.""" - return self.memory_store.get(key, "Key not found in memory") - - def recall_all(self): - return str(self.memory_store) - - def clear_all_conversation(self): - self.memory_store.clear() - - def recall_last_conversation(self, number_last_conversation): - if len(self.memory_store)>0: - max_key = max(self.memory_store.keys()) # Get the largest key - total_conversations = len(self.memory_store) # Get the total number of conversations - - if number_last_conversation >= total_conversations: - min_key = min(self.memory_store.keys()) # If size exceeds, start from the smallest key - else: - min_key = max_key - number_last_conversation + 1 # Calculate the starting key - return {k: self.memory_store[k] for k in range(min_key, max_key + 1)} - else: - return {} - - def forget(self, key): - """Removes a value from memory by its key.""" - if key in self.memory_store: - del self.memory_store[key] - logger.info(f"Forgot {key} from memory.") - else: - logger.warning(f"Key {key} not found in memory.") - - def clear_memory(self): - """Clears all stored memory.""" - self.memory_store.clear() - logger.info("Cleared all memory.") \ No newline at end of file diff --git a/build/lib/smartdata/modeler.py b/build/lib/smartdata/modeler.py deleted file mode 100644 index af06328..0000000 --- a/build/lib/smartdata/modeler.py +++ /dev/null @@ -1,267 +0,0 @@ -from langchain_openai import ChatOpenAI -from langchain.prompts import PromptTemplate -from langchain_core.output_parsers import StrOutputParser - -import pandas as pd -import numpy as np - -import base64 -import os -import io -import json -import ast -import copy -import logging -logger = logging.getLogger('SmartData') - -from .config import Config -from .memory import Memory # Import Memory from memory.py -from .custom_agent import * -from .util import * - -global config -config = dict(Config.__dict__) - -class SmartData: - def __init__(self, df_list, llm = None, show_detail = config['SHOW_DETAIL'], memory_size = config['MEMORY_SIZE'], - max_iterations = config['MAX_ITERATIONS'], max_execution_time = config['MAX_EXECUTION_TIME'], seed = 0): - - # Use ChatGPT 4o-mini by default - if llm is None: - chat_llm = ChatOpenAI(temperature=config['TEMP_CHAT'], model=config['CHAT_MODEL'], seed = seed) - self.llm = chat_llm - else: - self.llm = llm - - self.df_list = copy.deepcopy(df_list) - self.df_change = [] - self.memory_size = memory_size - self.max_iterations = max_iterations - self.max_execution_time = max_execution_time - self.show_detail = show_detail - self.image_fig_list = [] - self.check_error_substring_list = config['CHECK_ERROR_SUBSTRING_LIST'] - self.check_plot_substring_list = config['CHECK_PLOT_SUBSTRING_LIST'] - self.add_on_plot_library_list = config["ADD_ON_PLOT_LIBRARY_LIST"] - - self.check_datachange_substring_list = config['CHECK_DATACHANGE_SUBSTRING_LIST'] - self.add_on_datachange_library_list = config["ADD_ON_DATACHANGE_LIBRARY_LIST"] - - self.prompt_clean_data = config["PROMPT_CLEAN_DATA"] - self.prompt_create_data_clean_summary = config["PROMPT_CREATE_DATA_CLEAN_SUMMARY"] - - self.model = None - self.memory = Memory() - self.message_count = 1 - # self.df - # self.create_model() - - def create_model(self, use_openai_llm = True, seed = 0): - df = self.df_list - prefix_df = config['DEFAULT_PREFIX_SINGLE_DF'] - if use_openai_llm: - self.llm = ChatOpenAI(temperature=config['TEMP_CHAT'], model=config['CHAT_MODEL'], seed = seed) - - prompt, agent_executor = custom_create_pandas_dataframe_agent(llm = self.llm,df = df, - verbose=self.show_detail, - return_intermediate_steps = True, - agent_type="tool-calling", - allow_dangerous_code=True, - prefix = prefix_df, - max_iterations = self.max_iterations, - max_execution_time=self.max_execution_time, - agent_executor_kwargs={'handle_parsing_errors':True} - ) - self.model = agent_executor - return prompt, agent_executor - - def run_model(self, question): - for i in range(10): - prompt, _ = self.create_model(use_openai_llm = True, seed = i) - try: - # self.image_fig_list.clear() - self.image_fig_list.clear() - self.df_change.clear() - chat_model = self.model - code_list = [] - code_list_plot_wo_add_on = [] - code_list_plot_with_add_on = [] - - code_list_datachange_wo_add_on = [] - code_list_datachange_with_add_on = [] - has_plots = False - has_changes_to_df = False - new_prompt = None - - question_with_history = copy.deepcopy(question) - if self.memory.is_not_empty(): - question_with_history = f"My question is: {question}. Below is the our previous conversation and codes in chronological order, from the earliest to the latest.: {self.memory.recall_last_conversation(self.memory_size)}." - - response = chat_model.invoke({"input": question_with_history}) - answer = response['output'] - code_list = self.extract_code_from_response(response) - - # Process plot into fig ------------------------------------------------------------------------------------------------------------------------- - if len(code_list)>0: - code_list_plot_wo_add_on, code_list_plot_with_add_on = self.process_with_plot_code(code_list) - - if len(code_list_plot_with_add_on)>0: - for plot_code in code_list_plot_with_add_on: - exec(plot_code, {'image_fig_list': self.image_fig_list, 'df': self.df_list},{}) - if len(self.image_fig_list)>0: - has_plots = True - # print("no plot code") - - # Process data change into a new dataset -------------------------------------------------------------------------------------------------------- - if len(code_list)>0: - code_list_datachange_wo_add_on, code_list_datachange_with_add_on = self.process_with_datachange_code(code_list) - - if len(code_list_datachange_with_add_on)>0: - for data_code in code_list_datachange_with_add_on: - exec(data_code, {'df_change': self.df_change, 'df': self.df_list},{}) - # data_code_exe = True - # print("no plot code") - if len(self.df_change)>0: - has_changes_to_df = not self.df_list.equals(self.df_change[-1]) - self.df_list = copy.deepcopy(self.df_change[-1]) - new_prompt, _ = self.create_model(use_openai_llm = True, seed = i) - - # Store the chat history - self.remember_conversation(question, answer,code_list,code_list_plot_wo_add_on) - if any(error_substring in str(answer) for error_substring in config['AGENT_STOP_SUBSTRING_LIST']): - answer = config['AGENT_STOP_ANSWER'] - else: - break - except Exception as e: - print(f"Fail to process: {e}") - - return answer, has_plots, has_changes_to_df, self.image_fig_list, self.df_list, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on - # return answer, self.image_fig_list, response, code_list, code_list_plot_with_add_on, new_prompt - - def clean_data_without_ai(self): - df_clean_without_ai, summary_without_ai = clean_dataframe(df = self.df_list) - self.df_list = df_clean_without_ai - return summary_without_ai, df_clean_without_ai - - def clean_data_with_ai(self): - # data_before_ai = self.df_list - # self.df_list = data_before_ai - # new_prompt, _ = self.create_model(use_openai_llm = True, seed = 0) - # print(new_prompt) - answer, has_plots, has_changes_to_df, image_fig_list, df_new, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on = self.run_model(question = self.prompt_clean_data) - return answer, has_changes_to_df, df_new - - def clean_data(self): - summary = "" - summary_without_ai, df_clean_without_ai = self.clean_data_without_ai() - answer, has_changes_to_df, df_new = self.clean_data_with_ai() - summary = summary_without_ai + answer - _, final_summary = self.create_data_clean_summary(summary) - return final_summary, has_changes_to_df, self.df_list - - def create_data_clean_summary(self, result): - human_template = config['PROMPT_CREATE_DATA_CLEAN_SUMMARY'] - prompt_template_list = [human_template] - prompt_template= '\n\n'.join(prompt_template_list) - - summary_prompt = PromptTemplate(template = prompt_template,input_variables = ['result']) - message = summary_prompt - - summary_model = ChatOpenAI(temperature=config['TEMP_CHAT'],model=config['CHAT_MODEL'], seed = 0) - - chain = summary_prompt | summary_model | StrOutputParser() - answer = chain.invoke({"result": result, - }) - return message, answer - - def remember_conversation(self, question, answer,code_list, code_list_plot_wo_add_on): - self.memory.remember(key = self.message_count, role = 'Human', value = question) - self.memory.remember(key = self.message_count, role = 'AI', value = answer) - # self.memory.remember(key = self.message_count, role = 'All Codes', value = code_list) - self.memory.remember(key = self.message_count, role = 'Plot Code Generate By AI', value = code_list_plot_wo_add_on) - self.message_count = self.message_count + 1 - - def recall_all_conversation(self): - return self.memory.recall_all() - - def recall_last_conversation(self,number_last_conversation): - return self.memory.recall_last_conversation(number_last_conversation) - - def clear_all_conversation(self): - return self.memory.clear_all_conversation() - - def extract_code_from_response(self, response): - code_list = [] - try: - last_response = response['intermediate_steps'][-1] - if (len(last_response)>1 and len(str(last_response[1])) == 0) or (len(last_response)==1) or ((len(last_response)>1) and (not any(substring in str(last_response[1]).lower() for substring in self.check_error_substring_list))): - for tool_call in response['intermediate_steps'][-1][0].message_log[0].tool_calls: - # print("\n-----\n") - # print(call) - # print(call['name']) - # print(tool_call['args']['query']) - if tool_call['name'] == 'python_repl_ast': - code = tool_call['args']['query'] - code_list.append(code) - except: - code_list = [] - return code_list - - def process_with_plot_code(self, string_list): - # Filter only the code with all required plot substrings - code_list_plot_wo_add_on = [ - s for s in string_list - if all(substring in s for substring in self.check_plot_substring_list) - ] - - # Make sure no duplicates - code_list_plot_wo_add_on = list(dict.fromkeys(code_list_plot_wo_add_on)) - - # Add in the import library if they are missing from the plot to make it produce figs - for i in range(len(code_list_plot_wo_add_on)): - missing_imports = [ - library for library in self.add_on_plot_library_list if library not in code_list_plot_wo_add_on[i] - ] - if missing_imports: - # Add the missing imports at the top of the plot code - code_list_plot_wo_add_on[i] = "\n".join(missing_imports) + "\n" + code_list_plot_wo_add_on[i] - - # Add in the long label at the end - add_on_format_long_label = config['ADD_ON_FORMAT_LABEL_FOR_AXIS'] - code_list_plot_with_add_on_label = [ - code + add_on_format_long_label for code in code_list_plot_wo_add_on] - - # Add in the fig code at the end - add_on_fig = config['ADD_ON_FIG'] - code_list_plot_with_add_on_label_fig = [ - code + add_on_fig for code in code_list_plot_with_add_on_label - ] - - return code_list_plot_wo_add_on, code_list_plot_with_add_on_label_fig - - def process_with_datachange_code(self, string_list): - # Filter only the code with all required plot substrings - code_list_datachange_wo_add_on = [ - s for s in string_list - if all(substring in s for substring in self.check_datachange_substring_list) - ] - - # Make sure no duplicates - code_list_datachange_wo_add_on = list(dict.fromkeys(code_list_datachange_wo_add_on)) - - # Add in the import library if they are missing - for i in range(len(code_list_datachange_wo_add_on)): - missing_imports = [ - library for library in self.add_on_datachange_library_list if library not in code_list_datachange_wo_add_on[i] - ] - if missing_imports: - # Add the missing imports at the top of the plot code - code_list_datachange_wo_add_on[i] = "\n".join(missing_imports) + "\n" + code_list_datachange_wo_add_on[i] - - # Add in the df_change code at the end - add_on_df = config['ADD_ON_DF'] - code_list_datachange_with_add_on = [ - code + add_on_df for code in code_list_datachange_wo_add_on - ] - - return code_list_datachange_wo_add_on, code_list_datachange_with_add_on diff --git a/build/lib/smartdata/util.py b/build/lib/smartdata/util.py deleted file mode 100644 index 3308a86..0000000 --- a/build/lib/smartdata/util.py +++ /dev/null @@ -1,133 +0,0 @@ -import pandas as pd -import numpy as np - -def replace_invalid_values(x): - # Attempt to convert the value to a string and check for invalid values - if isinstance(x, str) or isinstance(x, (int, float)): - if str(x).strip().lower() in ['na', 'nan', 'not applicable', 'n/a', 'n.a.', 'null', 'empty', 'blank']: - return np.nan - # If x is a valid numeric value, return it as is - return x - -def clean_dataframe(df): - df_update = df.copy() - summary = { - 'numeric_columns_filled': {}, - 'numeric_outliers_capped': {}, - 'categorical_columns_filled': {}, - 'categorical_columns_removed': [], - 'datetime_columns_filled': {}, - 'rows_removed': 0, - 'columns_removed': 0 - } - - # 1. Remove empty rows and columns - rows_before = df_update.shape[0] - df_update.dropna(how='all', inplace=True) - rows_after = df_update.shape[0] - summary['rows_removed'] = rows_before - rows_after - - columns_before = df_update.shape[1] - df_update.dropna(axis=1, how='all', inplace=True) - columns_after = df_update.shape[1] - summary['columns_removed'] = columns_before - columns_after - - # 2. Clean numeric columns - for col in df_update.select_dtypes(include=[np.number]).columns: - # Replace invalid entries with NaN - # print(col) - df_update[col] = df_update[col].apply(replace_invalid_values) - - # Fill missing values with the mean - missing_count = df_update[col].isnull().sum() - if missing_count > 0: - mean_value = df_update[col].mean() - df_update[col].fillna(mean_value, inplace=True) - summary['numeric_columns_filled'][col] = missing_count - - # Detect outliers using IQR and cap them - # Q1 = df_update[col].quantile(0.25) - # Q3 = df_update[col].quantile(0.75) - # IQR = Q3 - Q1 - # lower_bound = Q1 - 2.5 * IQR - # upper_bound = Q3 + 2.5 * IQR - - lower_bound = df_update[col].quantile(0.01) - upper_bound = df_update[col].quantile(0.99) - - outliers_lower = df_update[df_update[col] < lower_bound][col].count() - outliers_upper = df_update[df_update[col] > upper_bound][col].count() - - if outliers_lower > 0 or outliers_upper > 0: - df_update[col] = np.where(df_update[col] < lower_bound, lower_bound, df_update[col]) - df_update[col] = np.where(df_update[col] > upper_bound, upper_bound, df_update[col]) - summary['numeric_outliers_capped'][col] = {'lower_capped': outliers_lower, 'upper_capped': outliers_upper} - - # 3. Clean categorical/string/object columns - for col in df_update.select_dtypes(include=['object']).columns: - # Replace invalid entries with NaN and trim spaces - - df_update[col] = df_update[col].apply(lambda x: replace_invalid_values(x)) - # Remove column if more than 90% of values are missing - missing_percentage = df_update[col].isnull().mean() - - df_update[col] = df_update[col].astype(str).str.strip() - df_update[col] = df_update[col].apply(lambda x: replace_invalid_values(x)) - # print(col) - # print(missing_percentage) - if missing_percentage > 0.9: - df_update.drop(columns=[col], inplace=True) - summary['categorical_columns_removed'].append(col) - else: - # Fill missing values with 'unknown' - missing_count = df_update[col].isnull().sum() - if missing_count > 0: - df_update[col].fillna('Not Specified', inplace=True) - summary['categorical_columns_filled'][col] = missing_count - - # 3. Clean datetime columns - for col in df_update.select_dtypes(include=['datetime']).columns: - # print(col) - try: - df_update[col] = pd.to_datetime(df_update[col], errors='coerce') - # Replace missing values with mode - missing_count = df_update[col].isnull().sum() - if missing_count > 0: - mode_value = df_update[col].mode()[0] - df_update[col].fillna(mode_value, inplace=True) - summary['datetime_columns_filled'][col] = missing_count - except Exception: - continue - - # Build the markdown summary string dynamically - summary_md = "**Data Cleaning Result:**\n\n" - - if summary['numeric_columns_filled']: - summary_md += "- Numeric columns with missing values filled using the column mean:\n " - summary_md += ', '.join([f"{col} ({count} values)" for col, count in summary['numeric_columns_filled'].items()]) + "\n\n" - - if summary['numeric_outliers_capped']: - summary_md += "- Numeric columns had outliers capped between the 1st and 99th percentiles:\n " - summary_md += ', '.join([f"{col} (lower capped: {caps['lower_capped']}, upper capped: {caps['upper_capped']})" for col, caps in summary['numeric_outliers_capped'].items()]) + "\n\n" - - if summary['categorical_columns_filled']: - summary_md += "- Categorical columns with missing values filled with 'Not Specified':\n " - summary_md += ', '.join([f"{col} ({count} values)" for col, count in summary['categorical_columns_filled'].items()]) + "\n\n" - - if summary['categorical_columns_removed']: - summary_md += "- Categorical columns removed due to over 90% missing data:\n " - summary_md += ', '.join(summary['categorical_columns_removed']) + "\n\n" - - if summary['datetime_columns_filled']: - summary_md += "- Datetime columns with missing values filled using the column mode:\n " - summary_md += ', '.join([f"{col} ({count} values)" for col, count in summary['datetime_columns_filled'].items()]) + "\n\n" - - summary_md += f"- Total number of rows removed: {summary['rows_removed']}\n" - summary_md += f"- Total number of columns removed: {summary['columns_removed']}\n\n" - - summary_md += "Next, we review and standardize categorical fields, identifying any unreasonable values.\n" - - # Output the summary - # print(summary_md) - - return df_update, summary_md \ No newline at end of file diff --git a/dist/smartdataai_test-2.3-py3-none-any.whl b/dist/smartdataai_test-2.3-py3-none-any.whl deleted file mode 100644 index 9105a84..0000000 Binary files a/dist/smartdataai_test-2.3-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.3.tar.gz b/dist/smartdataai_test-2.3.tar.gz deleted file mode 100644 index a8f33b8..0000000 Binary files a/dist/smartdataai_test-2.3.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.4-py3-none-any.whl b/dist/smartdataai_test-2.4-py3-none-any.whl deleted file mode 100644 index a3972ce..0000000 Binary files a/dist/smartdataai_test-2.4-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.4.tar.gz b/dist/smartdataai_test-2.4.tar.gz deleted file mode 100644 index 7ce5be1..0000000 Binary files a/dist/smartdataai_test-2.4.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.5-py3-none-any.whl b/dist/smartdataai_test-2.5-py3-none-any.whl deleted file mode 100644 index 0e58ae7..0000000 Binary files a/dist/smartdataai_test-2.5-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.5.tar.gz b/dist/smartdataai_test-2.5.tar.gz deleted file mode 100644 index bd48d87..0000000 Binary files a/dist/smartdataai_test-2.5.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.6-py3-none-any.whl b/dist/smartdataai_test-2.6-py3-none-any.whl deleted file mode 100644 index 6fdcb79..0000000 Binary files a/dist/smartdataai_test-2.6-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.6.tar.gz b/dist/smartdataai_test-2.6.tar.gz deleted file mode 100644 index d85f217..0000000 Binary files a/dist/smartdataai_test-2.6.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.7-py3-none-any.whl b/dist/smartdataai_test-2.7-py3-none-any.whl deleted file mode 100644 index 30d3cd4..0000000 Binary files a/dist/smartdataai_test-2.7-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.7.tar.gz b/dist/smartdataai_test-2.7.tar.gz deleted file mode 100644 index fdda563..0000000 Binary files a/dist/smartdataai_test-2.7.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.8-py3-none-any.whl b/dist/smartdataai_test-2.8-py3-none-any.whl deleted file mode 100644 index e724a32..0000000 Binary files a/dist/smartdataai_test-2.8-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.8.tar.gz b/dist/smartdataai_test-2.8.tar.gz deleted file mode 100644 index 2c17ae7..0000000 Binary files a/dist/smartdataai_test-2.8.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-2.9-py3-none-any.whl b/dist/smartdataai_test-2.9-py3-none-any.whl deleted file mode 100644 index 14c7a3a..0000000 Binary files a/dist/smartdataai_test-2.9-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-2.9.tar.gz b/dist/smartdataai_test-2.9.tar.gz deleted file mode 100644 index 4ca5194..0000000 Binary files a/dist/smartdataai_test-2.9.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-3.0-py3-none-any.whl b/dist/smartdataai_test-3.0-py3-none-any.whl deleted file mode 100644 index e089f8d..0000000 Binary files a/dist/smartdataai_test-3.0-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-3.0.tar.gz b/dist/smartdataai_test-3.0.tar.gz deleted file mode 100644 index 8af8c49..0000000 Binary files a/dist/smartdataai_test-3.0.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-4.0-py3-none-any.whl b/dist/smartdataai_test-4.0-py3-none-any.whl deleted file mode 100644 index 6027f74..0000000 Binary files a/dist/smartdataai_test-4.0-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-4.0.tar.gz b/dist/smartdataai_test-4.0.tar.gz deleted file mode 100644 index 90a70e8..0000000 Binary files a/dist/smartdataai_test-4.0.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-4.1-py3-none-any.whl b/dist/smartdataai_test-4.1-py3-none-any.whl deleted file mode 100644 index cc9cecb..0000000 Binary files a/dist/smartdataai_test-4.1-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-4.1.tar.gz b/dist/smartdataai_test-4.1.tar.gz deleted file mode 100644 index 8744a6a..0000000 Binary files a/dist/smartdataai_test-4.1.tar.gz and /dev/null differ diff --git a/dist/smartdataai_test-4.2-py3-none-any.whl b/dist/smartdataai_test-4.2-py3-none-any.whl deleted file mode 100644 index c2a6946..0000000 Binary files a/dist/smartdataai_test-4.2-py3-none-any.whl and /dev/null differ diff --git a/dist/smartdataai_test-4.2.tar.gz b/dist/smartdataai_test-4.2.tar.gz deleted file mode 100644 index e8cc2d6..0000000 Binary files a/dist/smartdataai_test-4.2.tar.gz and /dev/null differ diff --git a/example/Example1.ipynb b/example/Example1.ipynb new file mode 100644 index 0000000..9e21a32 --- /dev/null +++ b/example/Example1.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "id": "c7241e1a-5a1f-44d3-a8bb-29efa4ec28ed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We’ve made some great strides in cleaning up our data! Here’s a friendly overview of what we accomplished:\n", + "\n", + "- **Filled in missing values** for numeric columns using the average, like Age (177 values).\n", + "- **Capped outliers** for several columns to keep our data in check:\n", + " - Age: 7 to 8\n", + " - SibSp: 0 to 7\n", + " - Parch: 0 to 6\n", + " - Fare: 0 to 9\n", + "- **Categorical columns** with missing values were filled with 'Not Specified':\n", + " - Cabin (687 values) and Embarked (2 values).\n", + " \n", + "We didn’t remove any rows or columns, which is fantastic! We also standardized categories for **Sex**, **Embarked**, and **Cabin**, and replaced unreasonable values in **Age**, **Fare**, **SibSp**, and **Parch** with their averages. Overall, our dataset is now cleaner and ready for analysis!\n", + "has_changes_to_df: True\n", + " Survived Pclass \\\n", + "PassengerId \n", + "1 0 3 \n", + "2 1 1 \n", + "3 1 3 \n", + "4 1 1 \n", + "5 0 3 \n", + "\n", + " Name Sex Age \\\n", + "PassengerId \n", + "1 Braund, Mr. Owen Harris Male 22.0 \n", + "2 Cumings, Mrs. John Bradley (Florence Briggs Th... Female 38.0 \n", + "3 Heikkinen, Miss Laina Female 26.0 \n", + "4 Futrelle, Mrs. Jacques Heath (Lily May Peel) Female 35.0 \n", + "5 Allen, Mr. William Henry Male 35.0 \n", + "\n", + " SibSp Parch Ticket Fare Cabin Embarked \n", + "PassengerId \n", + "1 1.0 0.0 A/5 21171 7.2500 Unknown Southampton \n", + "2 1.0 0.0 PC 17599 71.2833 c85 Cherbourg \n", + "3 0.0 0.0 STON/O2. 3101282 7.9250 Unknown Southampton \n", + "4 1.0 0.0 113803 53.1000 c123 Southampton \n", + "5 0.0 0.0 373450 8.0500 Unknown Southampton \n" + ] + } + ], + "source": [ + "import os\n", + "import pandas as pd\n", + "from smartdata import SmartData\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "os.getenv('OPENAI_API_KEY')\n", + "\n", + "# Or Set OpenAI API key here :)\n", + "# os.environ[\"OPENAI_API_KEY\"] = \"Your openai key\"\n", + "\n", + "# Read sample data\n", + "df = pd.read_csv(r\"https://raw.githubusercontent.com/pandas-dev/pandas/main/doc/data/titanic.csv\", index_col=0)\n", + "\n", + "# Create SmartData Model\n", + "sd = SmartData(df, memory_size = 0, show_detail = False)\n", + "prompt, sd_model = sd.create_model()\n", + "\n", + "# Clean Data \n", + "# - summary: this is a summary of data cleaning result include action taken, impacted records etc. \n", + "# - has_changes_to_df: this is a boolean to indicate whether any changes to the existing df.\n", + "# - df_new: this is the new cleaned dataframe after all the clean process.\n", + "summary, has_changes_to_df, df_new = sd.clean_data()\n", + "print(summary)\n", + "print(\"has_changes_to_df: \"+str(has_changes_to_df))\n", + "print(df_new.head(5))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e300f3cd-a00e-4c0f-b6dd-96c662ed9008", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/example/Example2.ipynb b/example/Example2.ipynb new file mode 100644 index 0000000..5d4d1d9 --- /dev/null +++ b/example/Example2.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 27, + "id": "866fb42d-f81c-472f-a4ce-de6c698683da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------------Q1------------\n", + "\n", + "The average fare by sex is as follows:\n", + "\n", + "| Sex | Fare |\n", + "|----------|-----------|\n", + "| Female | 43.47 |\n", + "| Male | 24.56 |\n", + "has_plots - False\n", + "has_changes_to_df - False\n", + "\n", + "------------Q2------------\n", + "\n", + "I have created a bar chart displaying the average age by passenger class (Pclass). The chart effectively illustrates the relationship between the passenger class and the average age of passengers. If you have any further requests or need additional insights, feel free to ask!\n", + "has_plots - True\n", + "has_changes_to_df - False\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":28: UserWarning: FixedFormatter should only be used together with FixedLocator\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABHB0lEQVR4nO3de5yWc/748feITtImTTntlmKiGtWWDhJKBxIR67TsOrOW7DqszWFFDkvrEFHOWiFLCslhWUtOSbv4InSS1EpNWKPDdLh+f3g0P6ODpk+5Z7bn8/GYx+M7133d9/2ecX++O6+u67rvvCzLsgAAAEiwWa4HAAAAKj9hAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACQTFgAAQDJhAQAAJBMWAABAMmEBbNJ69OgRTZs2jb/85S+5HqXCGzNmTDRt2jRatmwZX331Va7HiQkTJkTTpk3jhhtu2OjP1bRp01W+dtttt2jdunX06dMnhgwZEosWLSr34958883RtGnTePXVVzfC1AA/rs1zPQBArrzxxhsxc+bMqFmzZjz66KPRr1+/qFq1aq7HqrBGjRoVNWvWjIULF8bo0aPj+OOPz/VIP6qtt946+vfvX/p9lmWxcOHCGD9+fNx8880xYcKEGD58eGy2mX+zAzZN/r8fsMkaNWpUVKlSJU466aQoKiqKv//977keqcKaNWtWTJw4MY488sj4yU9+Eg899FCuR/rR1axZM/r06VP6dcghh8QxxxwTQ4cOja5du8Ybb7zhNQRs0oQFsEkqLi6OZ555JgoLC6NPnz4RETFy5MgcT1VxjRo1KrIsi06dOkXXrl1j+vTpMWHChFyPVWEccsghERExceLE3A4CkEPCAtgkjRs3LhYtWhSdOnWKn/70p7H77rvHG2+8EdOmTYuIiKVLl8aee+4Z3bp1W+39DzvssGjfvn2UlJRERMSKFSvivvvuiz59+sTuu+8ebdu2jZNPPjkmTZpU5n4rz6n/xz/+Eb169YoWLVrE0UcfHRHfnlrzt7/9LY455pho27ZtNG/ePPbaa68455xzYubMmWUeJ8uyGD58eBx44IGx++67x3777Rd33HFH3HLLLdG0adP49NNPS/dd19nWZMWKFTFmzJioXr16tG3bNg444ICIiHjwwQdXu39JSUncfPPN0a1btygsLIxevXrFqFGj4qKLLoqmTZuW2XfJkiVxyy23xP777x8tWrSI9u3bR79+/eKjjz5ap9lWzjd48ODo3LlzFBYWRt++feOJJ54ovf3TTz+NXXfdNU455ZTVztq+ffs45phj1vn5VqdKlSoREbF8+fLSbVmWxUMPPRSHHXZYtG7dOjp27BinnXZa/N///d9aH6ukpCTuvPPO0vu1aNEi9t1337j44oujqKiozL6vvfZa/PrXv46OHTtGYWFhHHDAAXHjjTfG4sWLy+x3//33R9++fePnP/95tG7dOo444oh49NFHk35mgO9zjQWwSRo1alRERPTq1SsiIg488MB455134sEHH4yLL744tthiizj44IPjnnvuiX/961/x85//vPS+U6dOjXfffTeOO+640msyzj333Bg3blz07NkzjjjiiPjqq6/i0UcfjeOOOy6uv/762H///cs8/3nnnReHHXZY/PKXv4wtttgiIiKuvPLKuO+++6J79+5xzjnnRJZlMWnSpBg3blz8+9//jmeffbZ030svvTQeeuihaN++fRx99NExZ86cuOWWW6JatWqr/Kzlne37XnnllfjPf/4T+++/f9SoUSP23HPPqFOnTjz33HNRVFQU22yzTem+WZbFGWecEePHj4/u3bvHCSecEFOmTIlLLrkkttpqqzKPW1JSEieeeGK89dZb0adPnzj++ONj7ty5MXLkyDjiiCPi7rvvLvN7X5MRI0ZE9erV45e//GXUrFkzHnnkkTjvvPOiqKgojj/++Nhxxx2jffv28corr8T8+fOjXr16pff95z//GV9++WUcdthhP/g8a/PSSy9FRMTuu+9euu2iiy6KUaNGRdu2bePss8+OpUuXxogRI+K4446L++67LwoLC1f7WGeffXa88MIL0bdv3zjiiCNiyZIl8dJLL8XDDz8cU6ZMKT0N7a233opTTjklmjVrFr/5zW+iWrVq8corr8TQoUPj448/jhtvvDEiIu699964+uqr48ADD4wjjjgili5dGqNHj47+/fvH4sWLk6MKoFQGsImZOnVqVlBQkPXu3bt022effZbtuuuuWdu2bbOFCxdmWZZlU6ZMyQoKCrI//elPZe5/7bXXZgUFBdnkyZOzLMuyJ598MisoKMjuuOOOMvsVFxdn+++/f9a+ffvSx7zpppuygoKC7Pzzzy+z74IFC7JmzZplp5122irz9uvXLysoKMjeeeedLMuy7O23384KCgqy008/PVuxYkXpfm+++WbWtGnTrKCgIJs1a1a5Z1uTs88+OysoKMieeeaZ0m2XXHJJVlBQkA0bNqzMvk899VRWUFCQXX755WW2P/7441lBQUFWUFBQuu3222/PCgoKsnHjxpXZ9/PPP8/at2+f9erVa61zvf7661lBQUHWsmXL0p935c/WpUuXrGXLltlXX31V5vnvueeeMo9x+umnZ61atcqKi4vX+lwFBQXZPvvskxUVFZX5+vDDD7PBgwdnu+66a9ajR49s8eLFWZZl2RtvvJEVFBRk55xzTpn/RjNnzsyaNWuWnXHGGVmW/f/XwyuvvJJlWZZNnjx5tb+/LMuyww8/PCsoKMiKioqyLMuyyy67LCsoKMjmz59fZr9+/fplRx55ZLZkyZIsy7LswAMPzA444IAy+3zzzTfZgQcemF1yySVr/bkBysOpUMAm55FHHomIiN69e5dua9CgQbRt2zb++9//xpNPPhkRETvvvHO0bNkynn766TKnPD3xxBPRvHnz2HXXXSMiSvfv2bNnLFiwoPRryZIl0aNHj/jiiy9WOfd+zz33LPP91ltvHW+++eYqb3v73//+N2rUqBER314XEhHx1FNPRUTEqaeeGnl5eaX7tmnTZpXHXZ/ZvuvLL7+M559/PmrVqhX77LNP6faVv7uHHnooVqxYUbp93LhxERFx2mmnlXmcgw46KBo1arTKbLVr14727duXma1KlSqx9957x9SpU0tPTVubPn36xI477lj6/ZZbbhnHHHNMLFq0KMaPHx8R376tcO3ateOxxx4r3W/BggUxfvz42H///WPLLbf8wef5z3/+Ex07dizzddBBB8XQoUNj7733jnvvvbf0iNEzzzwTEREnnHBCmf9GP/vZz+KRRx6JSy+9dLXPseuuu8akSZPi3HPPLbO9qKio9IjPN998ExER2267bUREXHHFFTFp0qTS07AGDx4cI0eOLD2atu2228aMGTPi5ptvLv191qxZM8aOHRuXX375D/7cAOvKqVDAJmXZsmXx+OOPR0REy5Yty1yL0L59+3jjjTdi5MiRcfjhh0dExOGHHx6XXHJJvPjii9G9e/d49dVXY+7cuWX+cJ4xY0ZExBqvx4iImD17dpnvv3v60ErVqlWLf/zjH/H888/Hxx9/HLNnz465c+eW/mGaZVmZ59tpp51WeYwmTZrEK6+8kjTbdz3xxBNRUlISe+21V8ybN690+3bbbRd16tSJ2bNnx/jx40ujY8aMGbHllltG/fr1Vzvbxx9/XGa2xYsXR8eOHdc6W5MmTdZ4e8S3Afh9KyNm5bUp1apVi969e8cDDzwQU6ZMiV122SWeeOKJWLp0afTt23etj79SvXr1YtCgQaXf5+XlRa1atWKnnXaKWrVqldl35euqcePGqzzObrvtttbnqVq1aowbNy5eeeWVmDlzZnz66adRVFRU+jpYGXLHHXdc6aly48aNi6222ir22GOP6Nq1a/Tu3bs0SC+88MI444wzYsiQITFkyJBo0KBBdOrUKXr06BH77rtvmfABSCEsgE3KP//5z5g/f35ERPz6179e7T7/93//F++99140b948evXqFVdddVU89thj0b179xgzZkzpH6krrVixImrUqBG33nrrGp/3+xGw8mLflUpKSuKkk06KN954IwoLC0ufu1mzZvHiiy/GbbfdVrrv0qVLIyJW+5kb1atXL/P9+sz2XSuvRfnHP/4R//jHP1a7z4MPPlgaFkuXLl3jZ4GsbrYdd9wxBg4cuMbnX3lUaG3W9rkRm2/+//9n7rDDDosHHnggxowZE+eff36MGTMmfvazn0Xbtm1/8Dkivo2T7x8RWpOV/43K+0f7V199Fcccc0xMmzYt2rZtG7vvvnsceuihUVhYGMOHDy+N4oiIGjVqxG233RZTp06NF154IV5//fV47bXX4h//+Efccccd8be//S3q1KkTjRs3jnHjxsWkSZPipZdeitdffz0ee+yxePTRR6NHjx5x8803l2tGgDURFsAmZeUfyieffHK0bt16ldsfffTReP7552PkyJExcODAqFWrVvTs2TPGjRsX8+bNi+eeey66desWP/nJT0rvs+OOO8aMGTNil112ifz8/DKPN3ny5Pj8889L//V4TZ566ql444034qSTToo//OEPZW4bPXp0me932mmnePnll2P69OnRokWLMrdNnz69zPcps73//vsxefLk2H777eOiiy5a5faFCxfGBRdcEC+99FL85z//ie222y4aNWoUL7zwQixYsCDq1q37g7PNnTs39thjj9KL0lf617/+FYsWLVolRlbnk08+WWXbylN+vnv6VYsWLaJp06bxzDPPxFFHHRXvv/9+nH322RvlX+xXnpo1Y8aMaNasWZnbbrzxxvj666/j4osvXuV+999/f0ydOjX+9Kc/xS9/+csyt60M4pVmzJgRRUVF0bZt29h5553jlFNOiSVLlsRVV10VI0eOjLFjx8ZRRx0VH330UWy++eaxxx57xB577BER355adfrpp8ezzz4bH330URQUFGzIHx/YRLnGAthkzJ8/P1566aWoXbt2nHnmmdGtW7dVvs4+++yIiBg7dmzpNQ2HH354lJSUxGWXXRaLFi1a5R2EevbsGRHfntv+XcXFxfG73/0ufvvb38aSJUvWOtsXX3wREbHKH3gzZ84sPV9/2bJlERGlb/c6fPjw0tOjIiKmTJlS+u5EG2K2lRF21FFHrfZ3dfDBB0e3bt1i+fLlpe9UtPJdtoYPH17msV599dWYPHnyKrN98803cccdd5TZPnfu3PjNb34T55577jp9ivWTTz5Z+vuL+Pa6lPvuuy9q1669yhGGww8/PGbNmhVDhgyJzTbbLA499NAffPz10b1794iI+Otf/1pm+6xZs+Kee+6JTz75ZLVBs/Ln+P7b8v773/8uvRZm5evg8ssvj+OPPz7mzJlTul+1atWiefPmEfHtUbFly5bFscceG+edd17pUZSIb0/F+9nPfla6H8CG4IgFsMkYM2ZMLFu2LA499NA1/it906ZNY88994xXX301HnvssfjlL38Ze+yxRzRs2DD+/ve/x/bbb7/KNQF9+/aNp59+Oh5++OGYNWtW7LfffrFs2bJ4+OGH4+OPP47zzz8/GjRosNbZOnfuHNddd138+c9/jtmzZ0f9+vVjypQpMWrUqNI/JL/++uuI+PYi7b59+8ajjz4a8+fPj/322y/mzZsXI0aMKP1DfOUfres7W0lJSYwdOza22GKL+MUvfrHGuY8//vh49tln45FHHokzzzwzevfuHaNGjYphw4bFjBkzokOHDvHxxx/HAw88ENWrVy/z+QqnnHJKvPDCCzF48OCYPHlydOjQIf773//GyJEj47///W/85S9/WacjFsuXL4+jjjoqjj766Fi6dGk89NBDUVRUFIMGDVrl2oeDDjoorr322hgzZkx06tQptttuux98/PWx1157xcEHHxyjR4+Ozz77LPbbb79YuHBhPPDAA7H55pvHH//4x9Xer2vXrnHffffFH/7whzjmmGOidu3a8e6778bo0aOjSpUqsXTp0tLXwWmnnRZvvPFGHHPMMXHEEUdEfn5+6e96u+22i169ekX16tXj5JNPjsGDB8cvf/nL6NWrV9SoUSP+9a9/xdixY6NLly4/eA0LwDrL9dtSAfxYDjjggKxp06bZ9OnT17rfP//5z1XejnbYsGFZQUFBNnjw4NXep6SkJLvjjjuy3r17Z4WFhVm7du2yo48+usxbtGbZqm8v+l2vvvpqdtRRR2U///nPs5///OfZgQcemF1//fXZ+++/nxUUFGQXXHBB6b5Lly7Nhg4dmnXr1i1r3rx51rVr1+yee+4pfWvYzz//vNyzfdfKt6k955xz1vq7yrL//zaoTz/9dJZl376V6Z///Ods7733zpo3b5716tUre/zxx7Nf/OIXWYsWLcrct7i4OLv++uuzHj16ZM2bN886duyYnXjiidlrr732g8+78u1mR4wYkf35z3/OOnTokBUWFmZHHnlk9vLLL6/xfit/R0888cQPPsdKBQUFWZcuXdZ5/yzLsuXLl2fDhw/PevfunbVo0SLr1KlT1q9fv2zatGml+6zu9fDkk09mhxxySNaqVausXbt22SGHHJLdeeedpa/Lm266qXTfCRMmZCeffHLWqVOnrHnz5tk+++yTXXLJJdlnn31WZpaHH344O/zww7N27dplLVq0yHr16pXdcsstpW+PC7Ah5GXZd46jA1DhFRcXx2abbRY1a9Zc5bYTTzwxJkyYEG+//XaZC5d/LF9++WXUrFlztRdw9+zZM5YuXbrGi8B/LL///e/jlVdeifHjx6/2AwUBWD+usQCoZF566aVo3br1Khd1z5kzJ958881o0aJFTqIiIuJvf/tbtGzZMiZMmFBm+9tvvx0ff/xxtGrVKidzrTRr1qx47rnnom/fvqICYANzxAKgkvn666/jgAMOiIULF8bRRx8djRo1is8//zz+9re/xYIFC+Lee++NNm3a5GS2WbNmxcEHHxw1atSIo48+OrbddtuYNWtW6QfpPfLII9GwYcMffa6RI0fGpEmT4tVXX43FixfHuHHjfvC6FwDKR1gAVEKzZ8+O2267LV5++eWYN29e1K5dO9q0aRO/+c1vfvAD2Da2jz76KG677bZ48803o6ioKOrWrRudOnWKM844I37605/mZKaHH344rrrqqmjQoEFceumla/1QPgDWj7AAAACSucYCAABIJiwAAIBkwgIAAEj2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AAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQbL3CYtKkSdGvX7/o06dP/Oc//4nbb789nnzyyQ09GwAAUEmUOyyeffbZOPXUU2OHHXaIGTNmxLJly2LzzTePP/7xj/HAAw9sjBkBAIAKrtxhMWTIkBgwYEBccMEFUaVKlYiIOPHEE+Oqq66Ke+65Z4MPCAAAVHzlDouZM2dGq1atVtm+++67x9y5czfETAAAQCVT7rDYeeedY/z48atsHz16dOy8884bZCgAAKBy2by8d+jfv3+cfvrp8frrr8fSpUtj2LBhMXPmzHj33Xdj6NChG2NGAACggiv3EYu2bdvGU089FU2aNImuXbvGl19+Ga1atYpx48ZFx44dN8aMAABABVfuIxYREfn5+XH22Wdv6FkAAIBKqtxh0bVr18jLy1tle15eXmyxxRaRn58fBxxwQBx99NEbZEAAAKDiK3dYHHvssTFkyJA49thjo1WrVpFlWbz77rtx3333xWGHHRb169ePoUOHRnFxcZxyyikbY2YAAKCCKXdYjBkzJgYOHBgHHnhg6bb99tsvmjZtGsOGDYsxY8bEbrvtFhdffLGwAACATUS5L97+5JNPYtddd11l+y677BLTp0+PiIhGjRpFUVFR+nQAAEClUO6waNWqVdx8882xcOHC0m0LFy6MW265JXbfffeIiHjxxRejYcOGG25KAACgQiv3qVADBw6M008/PTp37hyNGjWKLMti5syZse2228bNN98cL7/8clx11VUxePDgjTEvAABQAZU7LH7605/G448/Hq+99lp89NFHUaVKldhll12iY8eOkZeXF3Xq1IkXX3wx6tatuzHmBQAAKqD1+hyLKlWqxF577RV7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Dx4cxx9/fJltH3zwQTRu3Dg3AwEAAGtVIcOiS5cuMXHixLjrrrvik08+iQceeCDGjBkTJ554Yq5HAwAAVqNChsXuu+8egwcPjsceeyx69+4d9913X1x33XXRunXrXI8GAACsRoW5xuLDDz8s8323bt2iW7duOZoGAAAojwp5xAIAAKhchAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJKkRYlJSURO/evWPChAml22bNmhXHH398tGrVKnr16hUvv/xyDicEAADWJudhsWTJkjjnnHNiypQppduyLIvf/va3Ua9evRg1alT06dMnzjzzzJgzZ04OJwUAANZk81w++dSpU+Pcc8+NLMvKbH/99ddj1qxZMXLkyKhZs2Y0adIkXnvttRg1alScddZZOZoWAABYk5wesXjjjTeiffv28dBDD5XZ/vbbb0ezZs2iZs2apdvatGkTb7311o88IQAAsC5yesTimGOOWe32efPmRf369cts22abbeKzzz77McYCAADKKadhsSaLFi2KqlWrltlWtWrVKCkpWev98vI25lTA6lh3ULlYs1C5VKY1WyHDolq1avHll1+W2VZSUhLVq1df433q1t0yqlTJ8bXoRcW5fX7IgXr1tsr1COvPmmUTZM1C5VKZ1myFDIsGDRrE1KlTy2ybP3/+KqdHfdeCBd9UqqKD/xXz53+d6xGAcrBmoXKpCGt2XeOmQoZFy5Yt4/bbb4/FixeXHqWYNGlStGnTZq33+96bSwE/AusOKhdrFiqXyrRmc/45FqvTrl272G677aJ///4xZcqUuP322+Odd96Jww8/PNejAQAAq1Ehw6JKlSpx6623xrx586Jv377x+OOPxy233BLbb799rkcDAABWo8KcCvXhhx+W+b5hw4YxYsSIHE0DAACUR4U8YgEAAFQuwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEhWocPi73//ezRt2rTMV79+/XI9FgAA8D2b53qAtZk6dWp06dIlBg4cWLqtWrVqOZwIAABYnQodFtOmTYuCgoLIz8/P9SgAAMBaVOhToaZNmxaNGjXK9RgAAMAPqLBHLLIsixkzZsTLL78ct912Wyxfvjz233//6NevX1StWnW198nL+5GHBKw7qGSsWahcKtOarbBhMWfOnFi0aFFUrVo1brzxxvj000/jiiuuiMWLF8fFF1+8yv51624ZVark+ABMUXFunx9yoF69rXI9wvqzZtkEWbNQuVSmNVthw2KHHXaICRMmxE9+8pPIy8uL3XbbLVasWBHnn39+9O/fP6pUqVJm/wULvqlURQf/K+bP/zrXIwDlYM1C5VIR1uy6xk2FDYuIiDp16pT5vkmTJrFkyZL46quvom7duqvsn2U/0mBAKesOKhdrFiqXyrRmK+zF2+PHj4/27dvHokWLSrdNnjw56tSps9qoAAAAcqfChkXr1q2jWrVqcfHFF8f06dPjxRdfjGuvvTZOPvnkXI8GAAB8T4U9FapWrVpx1113xVVXXRWHHXZYbLnllnHUUUcJCwAAqIAqbFhEROyyyy5xzz335HoMAADgB1TYU6EAAIDKQ1gAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQrEKHxZIlS+LCCy+Mtm3bxl577RV33313rkcCAABWY/NcD7A21157bbz77rsxfPjwmDNnTlxwwQWx/fbbx/7775/r0QAAgO+osGGxcOHCePjhh+OOO+6I5s2bR/PmzWPKlClx//33CwsAAKhgKuypUB988EEsW7YsWrduXbqtTZs28fbbb8eKFStyOBkAAPB9FTYs5s2bF1tvvXVUrVq1dFu9evViyZIl8eWXX+ZuMAAAYBUV9lSoRYsWlYmKiCj9vqSkZLX3ycvb6GMB32PdQeVizULlUpnWbIUNi2rVqq0SECu/r169+ir75+dv9aPMtTZ96+V+BmDdWbNQuVizULFV2FOhGjRoEF988UUsW7asdNu8efOievXqUbt27RxOBgAAfF+FDYvddtstNt9883jrrbdKt02aNCkKCwtjs80q7NgAALBJqrB/odeoUSMOOeSQGDBgQLzzzjvx3HPPxd133x2/+tWvcj0aAADwPRU2LCIi+vfvH82bN49f//rXcdlll8VZZ50VPXr0yPVYVEAlJSXRu3fvmDBhQq5HAdZi7ty50a9fv2jXrl107tw5rr766liyZEmuxwLWYObMmXHSSSdF69atY999940777wz1yNRgVXYi7cjvj1qcc0118Q111yT61GowJYsWRLnnntuTJkyJdejAGuRZVn069cvateuHffff3989dVXceGFF8Zmm20WF1xwQa7HA75nxYoVceqpp0ZhYWGMHj06Zs6cGeecc040aNAgDjrooFyPRwVUoY9YwA+ZOnVqHHHEEfHJJ5/kehTgB0yfPj3eeuutuPrqq2OXXXaJtm3bRr9+/WLs2LG5Hg1Yjfnz58duu+0WAwYMiEaNGsU+++wTHTt2jEmTJuV6NCooYUGl9sYbb0T79u3joYceyvUowA/Iz8+PO++8M+rVq1dme3FxcY4mAtamfv36ceONN0atWrUiy7KYNGlSTJw4Mdq1a5fr0aigKvSpUPBDjjnmmFyPAKyj2rVrR+fOnUu/X7FiRYwYMSI6dOiQw6mAddG1a9eYM2dOdOnSJXr27JnrcaigHLEAICcGDRoU77//fvz+97/P9SjAD7jpppti2LBhMXny5Lj66qtzPQ4VlCMWAPzoBg0aFMOHD48bbrghCgoKcj0O8AMKCwsj4ts3TDnvvPPiD3/4Q1StWjXHU1HROGIBwI9q4MCBcc8998SgQYOcUgEV2Pz58+O5554rs23nnXeOpUuXujaK1RIWAPxohgwZEiNHjozrr78+DjzwwFyPA6zFp59+GmeeeWbMnTu3dNu7774bdevWjbp16+ZwMioqYQHAj2LatGlx6623ximnnBJt2rSJefPmlX4BFU9hYWE0b948Lrzwwpg6dWq8+OKLMWjQoDj99NNzPRoVlGssAPhRPP/887F8+fIYOnRoDB06tMxtH374YY6mAtakSpUqceutt8bAgQPjyCOPjBo1asRxxx0Xv/rVr3I9GhVUXpZlWa6HAAAAKjenQgEAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYALBOunbtGk2bNi39at68eey///5x7733rtN9H3300Y0/JAA5s3muBwCg8rjwwgujV69eERGxbNmyeP311+Oiiy6KOnXqxCGHHJLb4QDIKUcsAFhnW221VeTn50d+fn5st912ceihh0bHjh3j2WefzfVoAOSYsAAgyeabbx5bbLFFLFu2LK6//vrYa6+9ok2bNtGvX7/44osvVtm/uLg4+vfvHx07dowWLVrE/vvvH88991zp7ePGjYuePXtGYWFh9OrVq8xtf/3rX6NLly5RWFgYffv2jTfffPNH+RkB+GHCAoD1snTp0nj22WfjlVdeif322y8GDx4co0ePjquuuioeeuihKCoqiksvvXSV+1155ZUxY8aMuPvuu2Ps2LHRtm3buOiii6KkpCSKioriD3/4Q5x22mnx9NNPx2GHHRbnnHNOfPnll/H+++/HtddeG5deemk89dRT0bZt2/jd734XK1asyMFPD8D3ucYCgHV26aWXxsCBAyMiYvHixVG9evX49a9/HQcddFB06NAhLrjggth7770jIuKyyy6Lp556apXH2GOPPeKEE06IgoKCiIg48cQT4+GHH46ioqL44osvYunSpbHtttvGDjvsECeeeGI0bdo0qlWrFrNnz468vLzYfvvtY8cdd4zf/e530aVLl1ixYkVstpl/JwPINWEBwDrr169f9OjRIyIiqlWrFvn5+VGlSpVYsGBBfPnll9G8efPSfXfeeec466yzVnmMQw45JJ577rn429/+FtOnT4/33nsvIiKWL18eu+22W+y7775xwgknxE477RT77bdf/OIXv4gaNWrEXnvtFQUFBXHQQQdFs2bNSm/bfHP/UwZQEfgnHgDW2TbbbBMNGzaMhg0bxrbbbhtVqlSJiCjXH/d/+MMf4pprronatWvH0UcfHbfddlvpbXl5eXHbbbfFww8/HD179owXXnghDj300Jg8eXLUqFEjHn744Rg+fHi0a9cuHn300ejbt2/MnTt3g/+cAJSfsAAgWe3atWPrrbeODz74oHTb5MmTY++9947FixeXbisuLo6xY8fGDTfcEP369Yvu3bvHV199FRERWZbFtGnT4pprrondd989fv/738eTTz4Z2223XYwfPz7+/e9/x2233RYdOnSI/v37x9NPPx1LliyJSZMm/eg/LwCrcvwYgA3iuOOOi8GDB0eDBg1im222iSuvvDJatWoV1atXL92natWqUaNGjXj22Wejbt26MWPGjLj88ssjIqKkpCRq164dDz74YGy11VZx0EEHxdSpU2P27NnRrFmzqF69etxyyy1Rr1696NixY0ycODEWLlwYTZs2zdWPDMB3CAsANohTTz01vv766/jd734Xy5Yti3333TcuueSSMvtUrVo1Bg0aFNdcc03cd999seOOO8ZvfvObuPHGG2Py5MnRu3fvuPnmm+Mvf/lLDBs2LLbZZps455xzYq+99oqIb99R6tZbb43LL788tt9++xg0aFA0adIkFz8uAN+Tl2VZlushAACAys01FgAAQDJhAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACQTFgAAQDJhAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACT7f8QSWo5NFg+oAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMWCAYAAABsvhCnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABHB0lEQVR4nO3de5yWc/748feITtImTTntlmKiGtWWDhJKBxIR67TsOrOW7DqszWFFDkvrEFHOWiFLCslhWUtOSbv4InSS1EpNWKPDdLh+f3g0P6ODpk+5Z7bn8/GYx+M7133d9/2ecX++O6+u67rvvCzLsgAAAEiwWa4HAAAAKj9hAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACQTFgAAQDJhAQAAJBMWAABAMmEBbNJ69OgRTZs2jb/85S+5HqXCGzNmTDRt2jRatmwZX331Va7HiQkTJkTTpk3jhhtu2OjP1bRp01W+dtttt2jdunX06dMnhgwZEosWLSr34958883RtGnTePXVVzfC1AA/rs1zPQBArrzxxhsxc+bMqFmzZjz66KPRr1+/qFq1aq7HqrBGjRoVNWvWjIULF8bo0aPj+OOPz/VIP6qtt946+vfvX/p9lmWxcOHCGD9+fNx8880xYcKEGD58eGy2mX+zAzZN/r8fsMkaNWpUVKlSJU466aQoKiqKv//977keqcKaNWtWTJw4MY488sj4yU9+Eg899FCuR/rR1axZM/r06VP6dcghh8QxxxwTQ4cOja5du8Ybb7zhNQRs0oQFsEkqLi6OZ555JgoLC6NPnz4RETFy5MgcT1VxjRo1KrIsi06dOkXXrl1j+vTpMWHChFyPVWEccsghERExceLE3A4CkEPCAtgkjRs3LhYtWhSdOnWKn/70p7H77rvHG2+8EdOmTYuIiKVLl8aee+4Z3bp1W+39DzvssGjfvn2UlJRERMSKFSvivvvuiz59+sTuu+8ebdu2jZNPPjkmTZpU5n4rz6n/xz/+Eb169YoWLVrE0UcfHRHfnlrzt7/9LY455pho27ZtNG/ePPbaa68455xzYubMmWUeJ8uyGD58eBx44IGx++67x3777Rd33HFH3HLLLdG0adP49NNPS/dd19nWZMWKFTFmzJioXr16tG3bNg444ICIiHjwwQdXu39JSUncfPPN0a1btygsLIxevXrFqFGj4qKLLoqmTZuW2XfJkiVxyy23xP777x8tWrSI9u3bR79+/eKjjz5ap9lWzjd48ODo3LlzFBYWRt++feOJJ54ovf3TTz+NXXfdNU455ZTVztq+ffs45phj1vn5VqdKlSoREbF8+fLSbVmWxUMPPRSHHXZYtG7dOjp27BinnXZa/N///d9aH6ukpCTuvPPO0vu1aNEi9t1337j44oujqKiozL6vvfZa/PrXv46OHTtGYWFhHHDAAXHjjTfG4sWLy+x3//33R9++fePnP/95tG7dOo444oh49NFHk35mgO9zjQWwSRo1alRERPTq1SsiIg488MB455134sEHH4yLL744tthiizj44IPjnnvuiX/961/x85//vPS+U6dOjXfffTeOO+640msyzj333Bg3blz07NkzjjjiiPjqq6/i0UcfjeOOOy6uv/762H///cs8/3nnnReHHXZY/PKXv4wtttgiIiKuvPLKuO+++6J79+5xzjnnRJZlMWnSpBg3blz8+9//jmeffbZ030svvTQeeuihaN++fRx99NExZ86cuOWWW6JatWqr/Kzlne37XnnllfjPf/4T+++/f9SoUSP23HPPqFOnTjz33HNRVFQU22yzTem+WZbFGWecEePHj4/u3bvHCSecEFOmTIlLLrkkttpqqzKPW1JSEieeeGK89dZb0adPnzj++ONj7ty5MXLkyDjiiCPi7rvvLvN7X5MRI0ZE9erV45e//GXUrFkzHnnkkTjvvPOiqKgojj/++Nhxxx2jffv28corr8T8+fOjXr16pff95z//GV9++WUcdthhP/g8a/PSSy9FRMTuu+9euu2iiy6KUaNGRdu2bePss8+OpUuXxogRI+K4446L++67LwoLC1f7WGeffXa88MIL0bdv3zjiiCNiyZIl8dJLL8XDDz8cU6ZMKT0N7a233opTTjklmjVrFr/5zW+iWrVq8corr8TQoUPj448/jhtvvDEiIu699964+uqr48ADD4wjjjgili5dGqNHj47+/fvH4sWLk6MKoFQGsImZOnVqVlBQkPXu3bt022effZbtuuuuWdu2bbOFCxdmWZZlU6ZMyQoKCrI//elPZe5/7bXXZgUFBdnkyZOzLMuyJ598MisoKMjuuOOOMvsVFxdn+++/f9a+ffvSx7zpppuygoKC7Pzzzy+z74IFC7JmzZplp5122irz9uvXLysoKMjeeeedLMuy7O23384KCgqy008/PVuxYkXpfm+++WbWtGnTrKCgIJs1a1a5Z1uTs88+OysoKMieeeaZ0m2XXHJJVlBQkA0bNqzMvk899VRWUFCQXX755WW2P/7441lBQUFWUFBQuu3222/PCgoKsnHjxpXZ9/PPP8/at2+f9erVa61zvf7661lBQUHWsmXL0p935c/WpUuXrGXLltlXX31V5vnvueeeMo9x+umnZ61atcqKi4vX+lwFBQXZPvvskxUVFZX5+vDDD7PBgwdnu+66a9ajR49s8eLFWZZl2RtvvJEVFBRk55xzTpn/RjNnzsyaNWuWnXHGGVmW/f/XwyuvvJJlWZZNnjx5tb+/LMuyww8/PCsoKMiKioqyLMuyyy67LCsoKMjmz59fZr9+/fplRx55ZLZkyZIsy7LswAMPzA444IAy+3zzzTfZgQcemF1yySVr/bkBysOpUMAm55FHHomIiN69e5dua9CgQbRt2zb++9//xpNPPhkRETvvvHO0bNkynn766TKnPD3xxBPRvHnz2HXXXSMiSvfv2bNnLFiwoPRryZIl0aNHj/jiiy9WOfd+zz33LPP91ltvHW+++eYqb3v73//+N2rUqBER314XEhHx1FNPRUTEqaeeGnl5eaX7tmnTZpXHXZ/ZvuvLL7+M559/PmrVqhX77LNP6faVv7uHHnooVqxYUbp93LhxERFx2mmnlXmcgw46KBo1arTKbLVr14727duXma1KlSqx9957x9SpU0tPTVubPn36xI477lj6/ZZbbhnHHHNMLFq0KMaPHx8R376tcO3ateOxxx4r3W/BggUxfvz42H///WPLLbf8wef5z3/+Ex07dizzddBBB8XQoUNj7733jnvvvbf0iNEzzzwTEREnnHBCmf9GP/vZz+KRRx6JSy+9dLXPseuuu8akSZPi3HPPLbO9qKio9IjPN998ExER2267bUREXHHFFTFp0qTS07AGDx4cI0eOLD2atu2228aMGTPi5ptvLv191qxZM8aOHRuXX375D/7cAOvKqVDAJmXZsmXx+OOPR0REy5Yty1yL0L59+3jjjTdi5MiRcfjhh0dExOGHHx6XXHJJvPjii9G9e/d49dVXY+7cuWX+cJ4xY0ZExBqvx4iImD17dpnvv3v60ErVqlWLf/zjH/H888/Hxx9/HLNnz465c+eW/mGaZVmZ59tpp51WeYwmTZrEK6+8kjTbdz3xxBNRUlISe+21V8ybN690+3bbbRd16tSJ2bNnx/jx40ujY8aMGbHllltG/fr1Vzvbxx9/XGa2xYsXR8eOHdc6W5MmTdZ4e8S3Afh9KyNm5bUp1apVi969e8cDDzwQU6ZMiV122SWeeOKJWLp0afTt23etj79SvXr1YtCgQaXf5+XlRa1atWKnnXaKWrVqldl35euqcePGqzzObrvtttbnqVq1aowbNy5eeeWVmDlzZnz66adRVFRU+jpYGXLHHXdc6aly48aNi6222ir22GOP6Nq1a/Tu3bs0SC+88MI444wzYsiQITFkyJBo0KBBdOrUKXr06BH77rtvmfABSCEsgE3KP//5z5g/f35ERPz6179e7T7/93//F++99140b948evXqFVdddVU89thj0b179xgzZkzpH6krrVixImrUqBG33nrrGp/3+xGw8mLflUpKSuKkk06KN954IwoLC0ufu1mzZvHiiy/GbbfdVrrv0qVLIyJW+5kb1atXL/P9+sz2XSuvRfnHP/4R//jHP1a7z4MPPlgaFkuXLl3jZ4GsbrYdd9wxBg4cuMbnX3lUaG3W9rkRm2/+//9n7rDDDosHHnggxowZE+eff36MGTMmfvazn0Xbtm1/8Dkivo2T7x8RWpOV/43K+0f7V199Fcccc0xMmzYt2rZtG7vvvnsceuihUVhYGMOHDy+N4oiIGjVqxG233RZTp06NF154IV5//fV47bXX4h//+Efccccd8be//S3q1KkTjRs3jnHjxsWkSZPipZdeitdffz0ee+yxePTRR6NHjx5x8803l2tGgDURFsAmZeUfyieffHK0bt16ldsfffTReP7552PkyJExcODAqFWrVvTs2TPGjRsX8+bNi+eeey66desWP/nJT0rvs+OOO8aMGTNil112ifz8/DKPN3ny5Pj8889L//V4TZ566ql444034qSTToo//OEPZW4bPXp0me932mmnePnll2P69OnRokWLMrdNnz69zPcps73//vsxefLk2H777eOiiy5a5faFCxfGBRdcEC+99FL85z//ie222y4aNWoUL7zwQixYsCDq1q37g7PNnTs39thjj9KL0lf617/+FYsWLVolRlbnk08+WWXbylN+vnv6VYsWLaJp06bxzDPPxFFHHRXvv/9+nH322RvlX+xXnpo1Y8aMaNasWZnbbrzxxvj666/j4osvXuV+999/f0ydOjX+9Kc/xS9/+csyt60M4pVmzJgRRUVF0bZt29h5553jlFNOiSVLlsRVV10VI0eOjLFjx8ZRRx0VH330UWy++eaxxx57xB577BER355adfrpp8ezzz4bH330URQUFGzIHx/YRLnGAthkzJ8/P1566aWoXbt2nHnmmdGtW7dVvs4+++yIiBg7dmzpNQ2HH354lJSUxGWXXRaLFi1a5R2EevbsGRHfntv+XcXFxfG73/0ufvvb38aSJUvWOtsXX3wREbHKH3gzZ84sPV9/2bJlERGlb/c6fPjw0tOjIiKmTJlS+u5EG2K2lRF21FFHrfZ3dfDBB0e3bt1i+fLlpe9UtPJdtoYPH17msV599dWYPHnyKrN98803cccdd5TZPnfu3PjNb34T55577jp9ivWTTz5Z+vuL+Pa6lPvuuy9q1669yhGGww8/PGbNmhVDhgyJzTbbLA499NAffPz10b1794iI+Otf/1pm+6xZs+Kee+6JTz75ZLVBs/Ln+P7b8v773/8uvRZm5evg8ssvj+OPPz7mzJlTul+1atWiefPmEfHtUbFly5bFscceG+edd17pUZSIb0/F+9nPfla6H8CG4IgFsMkYM2ZMLFu2LA499NA1/it906ZNY88994xXX301HnvssfjlL38Ze+yxRzRs2DD+/ve/x/bbb7/KNQF9+/aNp59+Oh5++OGYNWtW7LfffrFs2bJ4+OGH4+OPP47zzz8/GjRosNbZOnfuHNddd138+c9/jtmzZ0f9+vVjypQpMWrUqNI/JL/++uuI+PYi7b59+8ajjz4a8+fPj/322y/mzZsXI0aMKP1DfOUfres7W0lJSYwdOza22GKL+MUvfrHGuY8//vh49tln45FHHokzzzwzevfuHaNGjYphw4bFjBkzokOHDvHxxx/HAw88ENWrVy/z+QqnnHJKvPDCCzF48OCYPHlydOjQIf773//GyJEj47///W/85S9/WacjFsuXL4+jjjoqjj766Fi6dGk89NBDUVRUFIMGDVrl2oeDDjoorr322hgzZkx06tQptttuux98/PWx1157xcEHHxyjR4+Ozz77LPbbb79YuHBhPPDAA7H55pvHH//4x9Xer2vXrnHffffFH/7whzjmmGOidu3a8e6778bo0aOjSpUqsXTp0tLXwWmnnRZvvPFGHHPMMXHEEUdEfn5+6e96u+22i169ekX16tXj5JNPjsGDB8cvf/nL6NWrV9SoUSP+9a9/xdixY6NLly4/eA0LwDrL9dtSAfxYDjjggKxp06bZ9OnT17rfP//5z1XejnbYsGFZQUFBNnjw4NXep6SkJLvjjjuy3r17Z4WFhVm7du2yo48+usxbtGbZqm8v+l2vvvpqdtRRR2U///nPs5///OfZgQcemF1//fXZ+++/nxUUFGQXXHBB6b5Lly7Nhg4dmnXr1i1r3rx51rVr1+yee+4pfWvYzz//vNyzfdfKt6k955xz1vq7yrL//zaoTz/9dJZl376V6Z///Ods7733zpo3b5716tUre/zxx7Nf/OIXWYsWLcrct7i4OLv++uuzHj16ZM2bN886duyYnXjiidlrr732g8+78u1mR4wYkf35z3/OOnTokBUWFmZHHnlk9vLLL6/xfit/R0888cQPPsdKBQUFWZcuXdZ5/yzLsuXLl2fDhw/PevfunbVo0SLr1KlT1q9fv2zatGml+6zu9fDkk09mhxxySNaqVausXbt22SGHHJLdeeedpa/Lm266qXTfCRMmZCeffHLWqVOnrHnz5tk+++yTXXLJJdlnn31WZpaHH344O/zww7N27dplLVq0yHr16pXdcsstpW+PC7Ah5GXZd46jA1DhFRcXx2abbRY1a9Zc5bYTTzwxJkyYEG+//XaZC5d/LF9++WXUrFlztRdw9+zZM5YuXbrGi8B/LL///e/jlVdeifHjx6/2AwUBWD+usQCoZF566aVo3br1Khd1z5kzJ958881o0aJFTqIiIuJvf/tbtGzZMiZMmFBm+9tvvx0ff/xxtGrVKidzrTRr1qx47rnnom/fvqICYANzxAKgkvn666/jgAMOiIULF8bRRx8djRo1is8//zz+9re/xYIFC+Lee++NNm3a5GS2WbNmxcEHHxw1atSIo48+OrbddtuYNWtW6QfpPfLII9GwYcMffa6RI0fGpEmT4tVXX43FixfHuHHjfvC6FwDKR1gAVEKzZ8+O2267LV5++eWYN29e1K5dO9q0aRO/+c1vfvAD2Da2jz76KG677bZ48803o6ioKOrWrRudOnWKM844I37605/mZKaHH344rrrqqmjQoEFceumla/1QPgDWj7AAAACSucYCAABIJiwAAIBkwgIAAEj2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AAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQbL3CYtKkSdGvX7/o06dP/Oc//4nbb789nnzyyQ09GwAAUEmUOyyeffbZOPXUU2OHHXaIGTNmxLJly2LzzTePP/7xj/HAAw9sjBkBAIAKrtxhMWTIkBgwYEBccMEFUaVKlYiIOPHEE+Oqq66Ke+65Z4MPCAAAVHzlDouZM2dGq1atVtm+++67x9y5czfETAAAQCVT7rDYeeedY/z48atsHz16dOy8884bZCgAAKBy2by8d+jfv3+cfvrp8frrr8fSpUtj2LBhMXPmzHj33Xdj6NChG2NGAACggiv3EYu2bdvGU089FU2aNImuXbvGl19+Ga1atYpx48ZFx44dN8aMAABABVfuIxYREfn5+XH22Wdv6FkAAIBKqtxh0bVr18jLy1tle15eXmyxxRaRn58fBxxwQBx99NEbZEAAAKDiK3dYHHvssTFkyJA49thjo1WrVpFlWbz77rtx3333xWGHHRb169ePoUOHRnFxcZxyyikbY2YAAKCCKXdYjBkzJgYOHBgHHnhg6bb99tsvmjZtGsOGDYsxY8bEbrvtFhdffLGwAACATUS5L97+5JNPYtddd11l+y677BLTp0+PiIhGjRpFUVFR+nQAAEClUO6waNWqVdx8882xcOHC0m0LFy6MW265JXbfffeIiHjxxRejYcOGG25KAACgQiv3qVADBw6M008/PTp37hyNGjWKLMti5syZse2228bNN98cL7/8clx11VUxePDgjTEvAABQAZU7LH7605/G448/Hq+99lp89NFHUaVKldhll12iY8eOkZeXF3Xq1IkXX3wx6tatuzHmBQAAKqD1+hyLKlWqxF577RV7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Dx4cxx9/fJltH3zwQTRu3Dg3AwEAAGtVIcOiS5cuMXHixLjrrrvik08+iQceeCDGjBkTJ554Yq5HAwAAVqNChsXuu+8egwcPjsceeyx69+4d9913X1x33XXRunXrXI8GAACsRoW5xuLDDz8s8323bt2iW7duOZoGAAAojwp5xAIAAKhchAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJKkRYlJSURO/evWPChAml22bNmhXHH398tGrVKnr16hUvv/xyDicEAADWJudhsWTJkjjnnHNiypQppduyLIvf/va3Ua9evRg1alT06dMnzjzzzJgzZ04OJwUAANZk81w++dSpU+Pcc8+NLMvKbH/99ddj1qxZMXLkyKhZs2Y0adIkXnvttRg1alScddZZOZoWAABYk5wesXjjjTeiffv28dBDD5XZ/vbbb0ezZs2iZs2apdvatGkTb7311o88IQAAsC5yesTimGOOWe32efPmRf369cts22abbeKzzz77McYCAADKKadhsSaLFi2KqlWrltlWtWrVKCkpWev98vI25lTA6lh3ULlYs1C5VKY1WyHDolq1avHll1+W2VZSUhLVq1df433q1t0yqlTJ8bXoRcW5fX7IgXr1tsr1COvPmmUTZM1C5VKZ1myFDIsGDRrE1KlTy2ybP3/+KqdHfdeCBd9UqqKD/xXz53+d6xGAcrBmoXKpCGt2XeOmQoZFy5Yt4/bbb4/FixeXHqWYNGlStGnTZq33+96bSwE/AusOKhdrFiqXyrRmc/45FqvTrl272G677aJ///4xZcqUuP322+Odd96Jww8/PNejAQAAq1Ehw6JKlSpx6623xrx586Jv377x+OOPxy233BLbb799rkcDAABWo8KcCvXhhx+W+b5hw4YxYsSIHE0DAACUR4U8YgEAAFQuwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEgmLAAAgGTCAgAASCYsAACAZMICAABIJiwAAIBkwgIAAEhWocPi73//ezRt2rTMV79+/XI9FgAA8D2b53qAtZk6dWp06dIlBg4cWLqtWrVqOZwIAABYnQodFtOmTYuCgoLIz8/P9SgAAMBaVOhToaZNmxaNGjXK9RgAAMAPqLBHLLIsixkzZsTLL78ct912Wyxfvjz233//6NevX1StWnW198nL+5GHBKw7qGSsWahcKtOarbBhMWfOnFi0aFFUrVo1brzxxvj000/jiiuuiMWLF8fFF1+8yv51624ZVark+ABMUXFunx9yoF69rXI9wvqzZtkEWbNQuVSmNVthw2KHHXaICRMmxE9+8pPIy8uL3XbbLVasWBHnn39+9O/fP6pUqVJm/wULvqlURQf/K+bP/zrXIwDlYM1C5VIR1uy6xk2FDYuIiDp16pT5vkmTJrFkyZL46quvom7duqvsn2U/0mBAKesOKhdrFiqXyrRmK+zF2+PHj4/27dvHokWLSrdNnjw56tSps9qoAAAAcqfChkXr1q2jWrVqcfHFF8f06dPjxRdfjGuvvTZOPvnkXI8GAAB8T4U9FapWrVpx1113xVVXXRWHHXZYbLnllnHUUUcJCwAAqIAqbFhEROyyyy5xzz335HoMAADgB1TYU6EAAIDKQ1gAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYAAAAyYQFAACQrEKHxZIlS+LCCy+Mtm3bxl577RV33313rkcCAABWY/NcD7A21157bbz77rsxfPjwmDNnTlxwwQWx/fbbx/7775/r0QAAgO+osGGxcOHCePjhh+OOO+6I5s2bR/PmzWPKlClx//33CwsAAKhgKuypUB988EEsW7YsWrduXbqtTZs28fbbb8eKFStyOBkAAPB9FTYs5s2bF1tvvXVUrVq1dFu9evViyZIl8eWXX+ZuMAAAYBUV9lSoRYsWlYmKiCj9vqSkZLX3ycvb6GMB32PdQeVizULlUpnWbIUNi2rVqq0SECu/r169+ir75+dv9aPMtTZ96+V+BmDdWbNQuVizULFV2FOhGjRoEF988UUsW7asdNu8efOievXqUbt27RxOBgAAfF+FDYvddtstNt9883jrrbdKt02aNCkKCwtjs80q7NgAALBJqrB/odeoUSMOOeSQGDBgQLzzzjvx3HPPxd133x2/+tWvcj0aAADwPRU2LCIi+vfvH82bN49f//rXcdlll8VZZ50VPXr0yPVYVEAlJSXRu3fvmDBhQq5HAdZi7ty50a9fv2jXrl107tw5rr766liyZEmuxwLWYObMmXHSSSdF69atY999940777wz1yNRgVXYi7cjvj1qcc0118Q111yT61GowJYsWRLnnntuTJkyJdejAGuRZVn069cvateuHffff3989dVXceGFF8Zmm20WF1xwQa7HA75nxYoVceqpp0ZhYWGMHj06Zs6cGeecc040aNAgDjrooFyPRwVUoY9YwA+ZOnVqHHHEEfHJJ5/kehTgB0yfPj3eeuutuPrqq2OXXXaJtm3bRr9+/WLs2LG5Hg1Yjfnz58duu+0WAwYMiEaNGsU+++wTHTt2jEmTJuV6NCooYUGl9sYbb0T79u3joYceyvUowA/Iz8+PO++8M+rVq1dme3FxcY4mAtamfv36ceONN0atWrUiy7KYNGlSTJw4Mdq1a5fr0aigKvSpUPBDjjnmmFyPAKyj2rVrR+fOnUu/X7FiRYwYMSI6dOiQw6mAddG1a9eYM2dOdOnSJXr27JnrcaigHLEAICcGDRoU77//fvz+97/P9SjAD7jpppti2LBhMXny5Lj66qtzPQ4VlCMWAPzoBg0aFMOHD48bbrghCgoKcj0O8AMKCwsj4ts3TDnvvPPiD3/4Q1StWjXHU1HROGIBwI9q4MCBcc8998SgQYOcUgEV2Pz58+O5554rs23nnXeOpUuXujaK1RIWAPxohgwZEiNHjozrr78+DjzwwFyPA6zFp59+GmeeeWbMnTu3dNu7774bdevWjbp16+ZwMioqYQHAj2LatGlx6623ximnnBJt2rSJefPmlX4BFU9hYWE0b948Lrzwwpg6dWq8+OKLMWjQoDj99NNzPRoVlGssAPhRPP/887F8+fIYOnRoDB06tMxtH374YY6mAtakSpUqceutt8bAgQPjyCOPjBo1asRxxx0Xv/rVr3I9GhVUXpZlWa6HAAAAKjenQgEAAMmEBQAAkExYAAAAyYQFAACQTFgAAADJhAUAAJBMWAAAAMmEBQAAkExYALBOunbtGk2bNi39at68eey///5x7733rtN9H3300Y0/JAA5s3muBwCg8rjwwgujV69eERGxbNmyeP311+Oiiy6KOnXqxCGHHJLb4QDIKUcsAFhnW221VeTn50d+fn5st912ceihh0bHjh3j2WefzfVoAOSYsAAgyeabbx5bbLFFLFu2LK6//vrYa6+9ok2bNtGvX7/44osvVtm/uLg4+vfvHx07dowWLVrE/vvvH88991zp7ePGjYuePXtGYWFh9OrVq8xtf/3rX6NLly5RWFgYffv2jTfffPNH+RkB+GHCAoD1snTp0nj22WfjlVdeif322y8GDx4co0ePjquuuioeeuihKCoqiksvvXSV+1155ZUxY8aMuPvuu2Ps2LHRtm3buOiii6KkpCSKioriD3/4Q5x22mnx9NNPx2GHHRbnnHNOfPnll/H+++/HtddeG5deemk89dRT0bZt2/jd734XK1asyMFPD8D3ucYCgHV26aWXxsCBAyMiYvHixVG9evX49a9/HQcddFB06NAhLrjggth7770jIuKyyy6Lp556apXH2GOPPeKEE06IgoKCiIg48cQT4+GHH46ioqL44osvYunSpbHtttvGDjvsECeeeGI0bdo0qlWrFrNnz468vLzYfvvtY8cdd4zf/e530aVLl1ixYkVstpl/JwPINWEBwDrr169f9OjRIyIiqlWrFvn5+VGlSpVYsGBBfPnll9G8efPSfXfeeec466yzVnmMQw45JJ577rn429/+FtOnT4/33nsvIiKWL18eu+22W+y7775xwgknxE477RT77bdf/OIXv4gaNWrEXnvtFQUFBXHQQQdFs2bNSm/bfHP/UwZQEfgnHgDW2TbbbBMNGzaMhg0bxrbbbhtVqlSJiCjXH/d/+MMf4pprronatWvH0UcfHbfddlvpbXl5eXHbbbfFww8/HD179owXXnghDj300Jg8eXLUqFEjHn744Rg+fHi0a9cuHn300ejbt2/MnTt3g/+cAJSfsAAgWe3atWPrrbeODz74oHTb5MmTY++9947FixeXbisuLo6xY8fGDTfcEP369Yvu3bvHV199FRERWZbFtGnT4pprrondd989fv/738eTTz4Z2223XYwfPz7+/e9/x2233RYdOnSI/v37x9NPPx1LliyJSZMm/eg/LwCrcvwYgA3iuOOOi8GDB0eDBg1im222iSuvvDJatWoV1atXL92natWqUaNGjXj22Wejbt26MWPGjLj88ssjIqKkpCRq164dDz74YGy11VZx0EEHxdSpU2P27NnRrFmzqF69etxyyy1Rr1696NixY0ycODEWLlwYTZs2zdWPDMB3CAsANohTTz01vv766/jd734Xy5Yti3333TcuueSSMvtUrVo1Bg0aFNdcc03cd999seOOO8ZvfvObuPHGG2Py5MnRu3fvuPnmm+Mvf/lLDBs2LLbZZps455xzYq+99oqIb99R6tZbb43LL788tt9++xg0aFA0adIkFz8uAN+Tl2VZlushAACAys01FgAAQDJhAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACQTFgAAQDJhAQAAJBMWAABAMmEBAAAkExYAAEAyYQEAACT7f8QSWo5NFg+oAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------------Q3------------\n", + "\n", + "I have successfully created a new column called `age_over_30` in the dataframe. This column indicates whether the age of each passenger is over 30, with valid entries being 'yes' or 'no'. \n", + "\n", + "Here are the first few entries of the updated dataframe:\n", + "\n", + "| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | age_over_30 |\n", + "|--------------:|-----------:|---------:|:----------------------------------------------------|:-------|------:|--------:|--------:|:-----------------|--------:|:--------------|:------------|:------------|\n", + "| 1 | 0 | 3 | Braund, Mr. Owen Harris | Male | 22 | 1 | 0 | A/5 21171 | 7.25 | Not Specified | Southampton | no |\n", + "| 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | Female | 38 | 1 | 0 | PC 17599 | 71.2833 | C85 | Cherbourg | yes |\n", + "| 3 | 1 | 3 | Heikkinen, Miss Laina | Female | 26 | 0 | 0 | STON/O2. 3101282 | 7.925 | Not Specified | Southampton | no |\n", + "| 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | Female | 35 | 1 | 0 | 113803 | 53.1 | C123 | Southampton | yes |\n", + "| 5 | 0 | 3 | Allen, Mr. William Henry | Male | 35 | 0 | 0 | 373450 | 8.05 | Not Specified | Southampton | yes |\n", + "\n", + "If you have any further requests or need additional insights, feel free to ask!\n", + "has_plots - False\n", + "has_changes_to_df - True\n", + " Survived Pclass \\\n", + "PassengerId \n", + "1 0 3 \n", + "2 1 1 \n", + "3 1 3 \n", + "\n", + " Name Sex Age \\\n", + "PassengerId \n", + "1 Braund, Mr. Owen Harris Male 22.0 \n", + "2 Cumings, Mrs. John Bradley (Florence Briggs Th... Female 38.0 \n", + "3 Heikkinen, Miss Laina Female 26.0 \n", + "\n", + " SibSp Parch Ticket Fare Cabin \\\n", + "PassengerId \n", + "1 1.0 0.0 A/5 21171 7.2500 Not Specified \n", + "2 1.0 0.0 PC 17599 71.2833 C85 \n", + "3 0.0 0.0 STON/O2. 3101282 7.9250 Not Specified \n", + "\n", + " Embarked age_over_30 \n", + "PassengerId \n", + "1 Southampton no \n", + "2 Cherbourg yes \n", + "3 Southampton no \n", + "\n", + "------------Q4------------\n", + "\n", + "The column `age_over_30` has been successfully removed from the copied dataframe. If you have any further requests or need additional insights, feel free to ask!\n", + "has_plots - False\n", + "has_changes_to_df - True\n", + " Survived Pclass \\\n", + "PassengerId \n", + "1 0 3 \n", + "2 1 1 \n", + "3 1 3 \n", + "\n", + " Name Sex Age \\\n", + "PassengerId \n", + "1 Braund, Mr. Owen Harris Male 22.0 \n", + "2 Cumings, Mrs. John Bradley (Florence Briggs Th... Female 38.0 \n", + "3 Heikkinen, Miss Laina Female 26.0 \n", + "\n", + " SibSp Parch Ticket Fare Cabin \\\n", + "PassengerId \n", + "1 1.0 0.0 A/5 21171 7.2500 Not Specified \n", + "2 1.0 0.0 PC 17599 71.2833 C85 \n", + "3 0.0 0.0 STON/O2. 3101282 7.9250 Not Specified \n", + "\n", + " Embarked \n", + "PassengerId \n", + "1 Southampton \n", + "2 Cherbourg \n", + "3 Southampton \n" + ] + } + ], + "source": [ + "import os\n", + "import pandas as pd\n", + "from smartdata import SmartData\n", + "from dotenv import load_dotenv\n", + "from matplotlib import pyplot as plt\n", + "\n", + "load_dotenv()\n", + "os.getenv('OPENAI_API_KEY')\n", + "\n", + "# Or Set OpenAI API key here :)\n", + "# os.environ[\"OPENAI_API_KEY\"] = \"Your openai key\"\n", + "\n", + "# Load sample data\n", + "df_clean = pd.read_csv(r\"https://raw.githubusercontent.com/pandas-dev/pandas/main/doc/data/titanic.csv\", index_col=0)\n", + "\n", + "# Initialize SmartData Model to clean up data\n", + "sd_clean = SmartData(df_list=df_clean, memory_size=0, show_detail=False)\n", + "prompt, sd_model = sd_clean.create_model()\n", + "summary, has_changes_to_df, df_new = sd_clean.clean_data()\n", + "\n", + "# Initialize SmartData Model with memory for the last 3 conversations and detailed outputs\n", + "# Load in cleaned data\n", + "smartdata_qa = SmartData(df_list=df_new, memory_size=3, show_detail=False)\n", + "qa_prompt, qa_model = smartdata_qa.create_model()\n", + "\n", + "# Start Q&A session -------------------------------------------------\n", + "\n", + "# Output Explanation:\n", + "# answer: The response to your question, formatted in markdown.\n", + "# has_plots: Boolean indicating if a chart was generated.\n", + "# has_changes_to_df: Boolean indicating if the dataframe was updated.\n", + "# image_fig_list: List of matplotlib figures (if has_plots is True).\n", + "# df_new: Updated dataframe (if has_changes_to_df is True); otherwise, a copy of the original dataframe.\n", + "# response: Detailed output of all intermediate steps generated by the model.\n", + "# code_list_plot_with_add_on: Python code to generate the figures in image_fig_list.\n", + "# code_list_datachange_with_add_on: Python code to apply the dataframe updates resulting in df_new.\n", + "\n", + "# Q1 - General analytics question - no charting no new dataframe\n", + "question_1 = \"Please show me the average fare by sex in a table.\"\n", + "answer, has_plots, has_changes_to_df, image_fig_list, df_new, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on = smartdata_qa.run_model(question=question_1)\n", + "print(\"\\n------------Q1------------\\n\")\n", + "print(answer)\n", + "print(\"has_plots - \" + str(has_plots))\n", + "print(\"has_changes_to_df - \" + str(has_changes_to_df))\n", + "\n", + "# Q2 - Ask for making a chart\n", + "question_2 = \"Please make a bar chart with average Age by Pclass.\"\n", + "answer, has_plots, has_changes_to_df, image_fig_list, df_new, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on = smartdata_qa.run_model(question=question_2)\n", + "print(\"\\n------------Q2------------\\n\")\n", + "print(answer)\n", + "print(\"has_plots - \" + str(has_plots))\n", + "print(\"has_changes_to_df - \" + str(has_changes_to_df))\n", + "for fig in image_fig_list:\n", + " plt.show(fig)\n", + "\n", + "# Q3 - Ask for data transformation\n", + "question_3 = \"Can you create a new column called age over 30, valid entries are yes or no.\"\n", + "answer, has_plots, has_changes_to_df, image_fig_list, df_new, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on = smartdata_qa.run_model(question=question_3)\n", + "print(\"\\n------------Q3------------\\n\")\n", + "print(answer)\n", + "print(\"has_plots - \" + str(has_plots))\n", + "print(\"has_changes_to_df - \" + str(has_changes_to_df))\n", + "print(df_new.head(3))\n", + "\n", + "# Q4 - Chat with memory\n", + "question_4 = \"Can you delete the new column you just created?\"\n", + "answer, has_plots, has_changes_to_df, image_fig_list, df_new, response, code_list, code_list_plot_with_add_on, code_list_datachange_with_add_on = smartdata_qa.run_model(question=question_4)\n", + "print(\"\\n------------Q4------------\\n\")\n", + "print(answer)\n", + "print(\"has_plots - \" + str(has_plots))\n", + "print(\"has_changes_to_df - \" + str(has_changes_to_df))\n", + "print(df_new.head(3))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68209c63-964c-4a09-8fe3-6169869e49e2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/g.bat b/g.bat deleted file mode 100644 index fb03ce6..0000000 --- a/g.bat +++ /dev/null @@ -1,2 +0,0 @@ -python setup.py sdist bdist_wheel -twine upload dist/* diff --git a/setup.py b/setup.py deleted file mode 100644 index 4820877..0000000 --- a/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -from setuptools import setup, find_packages - -with open("README.md", "r", encoding="utf-8") as fh: - long_description = fh.read() - -setup( - name="smartdataai_test", - version="4.2", - packages=find_packages(), - install_requires=[ - 'pandas', - 'numpy', - 'matplotlib', - 'seaborn', - 'statsmodels', - 'scipy', - 'scikit-learn', - 'langchain', - 'langchain-community', - 'langchain-core', - 'langchain-experimental', - 'langchain-openai' - ], - include_package_data=True, - description='A package for SmartData management and operations.', - long_description=long_description, - long_description_content_type="text/markdown", # Ensure this is correct for Markdown - author='Talent AI Now', - author_email='contact@talentainow.com', - url='https://github.com/yourusername/smartdataai', # Update with your repository URL - classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - ], - python_requires='>=3.6', -) diff --git a/smartdataai_test.egg-info/PKG-INFO b/smartdataai_test.egg-info/PKG-INFO deleted file mode 100644 index 422f6b3..0000000 --- a/smartdataai_test.egg-info/PKG-INFO +++ /dev/null @@ -1,62 +0,0 @@ -Metadata-Version: 2.1 -Name: smartdataai_test -Version: 4.2 -Summary: A package for SmartData management and operations. -Home-page: https://github.com/yourusername/smartdataai -Author: Talent AI Now -Author-email: contact@talentainow.com -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 -Description-Content-Type: text/markdown -Requires-Dist: pandas -Requires-Dist: numpy -Requires-Dist: matplotlib -Requires-Dist: seaborn -Requires-Dist: statsmodels -Requires-Dist: scipy -Requires-Dist: scikit-learn -Requires-Dist: langchain -Requires-Dist: langchain-community -Requires-Dist: langchain-core -Requires-Dist: langchain-experimental -Requires-Dist: langchain-openai - -# Foobar - -Foobar is a Python library for dealing with word pluralization. - -## Installation - -Use the package manager [pip](https://pip.pypa.io/en/stable/) to install foobar. - -```bash -pip install foobar -``` - -## Usage - -```python -import foobar - -# returns 'words' -foobar.pluralize('word') - -# returns 'geese' -foobar.pluralize('goose') - -# returns 'phenomenon' -foobar.singularize('phenomena') -``` - -## Contributing - -Pull requests are welcome. For major changes, please open an issue first -to discuss what you would like to change. - -Please make sure to update tests as appropriate. - -## License - -[MIT](https://choosealicense.com/licenses/mit/) diff --git a/smartdataai_test.egg-info/SOURCES.txt b/smartdataai_test.egg-info/SOURCES.txt deleted file mode 100644 index 59644cb..0000000 --- a/smartdataai_test.egg-info/SOURCES.txt +++ /dev/null @@ -1,13 +0,0 @@ -README.md -setup.py -smartdata/__init__.py -smartdata/config.py -smartdata/custom_agent.py -smartdata/memory.py -smartdata/modeler.py -smartdata/util.py -smartdataai_test.egg-info/PKG-INFO -smartdataai_test.egg-info/SOURCES.txt -smartdataai_test.egg-info/dependency_links.txt -smartdataai_test.egg-info/requires.txt -smartdataai_test.egg-info/top_level.txt \ No newline at end of file diff --git a/smartdataai_test.egg-info/dependency_links.txt b/smartdataai_test.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/smartdataai_test.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/smartdataai_test.egg-info/requires.txt b/smartdataai_test.egg-info/requires.txt deleted file mode 100644 index 9dd5997..0000000 --- a/smartdataai_test.egg-info/requires.txt +++ /dev/null @@ -1,12 +0,0 @@ -pandas -numpy -matplotlib -seaborn -statsmodels -scipy -scikit-learn -langchain -langchain-community -langchain-core -langchain-experimental -langchain-openai diff --git a/smartdataai_test.egg-info/top_level.txt b/smartdataai_test.egg-info/top_level.txt deleted file mode 100644 index e273488..0000000 --- a/smartdataai_test.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -smartdata