Files
OpenBB/openbb_terminal/econometrics/econometrics_controller.py
montezdesousa 02901f1473 Trim command output spaces (#3424)
* first round of removing spaces

* remove more print statemnts

* remove more spaces

* bullbear color

* spacc color

* improve stalker readability

* delete more spaces

* reddit colors

* rever hood

* flake8

* forgot this one

* fix econometrics tests

* fix econometrics tests

* fix more tests

Co-authored-by: James Maslek <jmaslek11@gmail.com>
2022-11-16 11:47:24 +00:00

1759 lines
63 KiB
Python

"""Econometrics Controller Module"""
__docformat__ = "numpy"
# pylint: disable=too-many-lines, too-many-branches, inconsistent-return-statements
import argparse
import logging
import os
from itertools import chain
from pathlib import Path
from typing import Any, Dict, List
import numpy as np
import pandas as pd
from openbb_terminal import feature_flags as obbff
from openbb_terminal.common import common_model
from openbb_terminal.core.config.paths import (
USER_CUSTOM_IMPORTS_DIRECTORY,
USER_EXPORTS_DIRECTORY,
)
from openbb_terminal.custom_prompt_toolkit import NestedCompleter
from openbb_terminal.decorators import log_start_end
from openbb_terminal.econometrics import (
econometrics_model,
econometrics_view,
regression_model,
regression_view,
)
from openbb_terminal.helper_funcs import (
EXPORT_BOTH_RAW_DATA_AND_FIGURES,
EXPORT_ONLY_FIGURES_ALLOWED,
EXPORT_ONLY_RAW_DATA_ALLOWED,
NO_EXPORT,
check_list_values,
check_positive,
check_positive_float,
export_data,
print_rich_table,
)
from openbb_terminal.menu import session
from openbb_terminal.parent_classes import BaseController
from openbb_terminal.rich_config import MenuText, console
logger = logging.getLogger(__name__)
# pylint: disable=R0902,C0302
class EconometricsController(BaseController):
"""Econometrics class"""
CHOICES_COMMANDS: List[str] = [
"load",
"export",
"remove",
"plot",
"show",
"type",
"desc",
"index",
"clean",
"add",
"delete",
"combine",
"rename",
"ols",
"norm",
"root",
"panel",
"compare",
"dwat",
"bgod",
"bpag",
"granger",
"coint",
]
CHOICES_MENUS: List[str] = [
"qa",
]
pandas_plot_choices = [
"line",
"scatter",
"bar",
"barh",
"hist",
"box",
"kde",
"area",
"pie",
"hexbin",
]
PANEL_CHOICES = [
"pols",
"re",
"bols",
"fe",
"fdols",
"POLS",
"RE",
"BOLS",
"FE",
"FDOLS",
]
PATH = "/econometrics/"
loaded_dataset_cols = "\n"
list_dataset_cols: List = list()
def __init__(self, queue: List[str] = None):
"""Constructor"""
super().__init__(queue)
self.files: List[str] = list()
self.datasets: Dict[str, pd.DataFrame] = dict()
self.regression: Dict[Any[Dict, Any], Any] = dict()
self.DATA_TYPES: List[str] = ["int", "float", "str", "bool", "category", "date"]
for regression in [
"OLS",
"POLS",
"BOLS",
"RE",
"FE",
"FE_EE",
"FE_IE",
"FE_EE_IE",
"FDOLS",
]:
self.regression[regression] = {
"data": {},
"independent": {},
"dependent": {},
"model": {},
}
self.signs: Dict[Any, Any] = {
"div": "/",
"mul": "*",
"add": "+",
"sub": "-",
"mod": "%",
"pow": "**",
}
self.DATA_FILES = {
filepath.name: filepath
for file_type in common_model.file_types
for filepath in chain(
Path(USER_EXPORTS_DIRECTORY).rglob(f"*.{file_type}"),
Path(USER_CUSTOM_IMPORTS_DIRECTORY / "econometrics").rglob(
f"*.{file_type}"
),
)
if filepath.is_file()
}
if session and obbff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.controller_choices}
choices["load"] = {
"--file": {c: {} for c in self.DATA_FILES.keys()},
"-f": "--file",
"-alias": None,
"-a": "-alias",
"--examples": None,
"-e": "--examples",
}
for feature in ["export", "show", "desc", "clear", "index"]:
choices[feature] = {c: {} for c in self.files}
for feature in [
"type",
"plot",
"norm",
"root",
"granger",
"coint",
]:
choices[feature] = dict()
# Inititialzie this for regressions to be able to use -h flag
choices["regressions"] = {}
self.choices = choices
choices["support"] = self.SUPPORT_CHOICES
choices["about"] = self.ABOUT_CHOICES
choices["panel"]["-r"] = {c: {} for c in self.PANEL_CHOICES}
self.completer = NestedCompleter.from_nested_dict(choices)
def update_runtime_choices(self):
if session and obbff.USE_PROMPT_TOOLKIT:
dataset_columns = {
f"{dataset}.{column}": {}
for dataset, dataframe in self.datasets.items()
for column in dataframe.columns
}
for feature in [
"plot",
"norm",
"root",
"coint",
"regressions",
"ols",
"panel",
"delete",
]:
self.choices[feature] = dataset_columns
for feature in [
"export",
"show",
"clean",
"index",
"remove",
"combine",
"rename",
]:
self.choices[feature] = {c: {} for c in self.files}
self.choices["type"] = {
c: {} for c in self.files + list(dataset_columns.keys())
}
self.choices["desc"] = {
c: {} for c in self.files + list(dataset_columns.keys())
}
pairs_timeseries = list()
for dataset_col in list(dataset_columns.keys()):
pairs_timeseries += [
f"{dataset_col},{dataset_col2}"
for dataset_col2 in list(dataset_columns.keys())
if dataset_col != dataset_col2
]
self.choices["granger"] = {c: {} for c in pairs_timeseries}
self.completer = NestedCompleter.from_nested_dict(self.choices)
def print_help(self):
"""Print help"""
mt = MenuText("econometrics/")
mt.add_param(
"_data_loc",
f"\n\t{str(USER_EXPORTS_DIRECTORY)}\n\t{str(USER_CUSTOM_IMPORTS_DIRECTORY/'econometrics')}",
)
mt.add_raw("\n")
mt.add_cmd("load")
mt.add_cmd("remove", self.files)
mt.add_raw("\n")
mt.add_param("_loaded", self.loaded_dataset_cols)
mt.add_info("_exploration_")
mt.add_cmd("show", self.files)
mt.add_cmd("plot", self.files)
mt.add_cmd("type", self.files)
mt.add_cmd("desc", self.files)
mt.add_cmd("index", self.files)
mt.add_cmd("clean", self.files)
mt.add_cmd("add", self.files)
mt.add_cmd("delete", self.files)
mt.add_cmd("combine", self.files)
mt.add_cmd("rename", self.files)
mt.add_cmd("export", self.files)
mt.add_info("_tests_")
mt.add_cmd("norm", self.files)
mt.add_cmd("root", self.files)
mt.add_cmd("granger", self.files)
mt.add_cmd("coint", self.files)
mt.add_info("_regression_")
mt.add_cmd("ols", self.files)
mt.add_cmd("panel", self.files)
mt.add_cmd("compare", self.files)
mt.add_info("_regression_tests_")
mt.add_cmd("dwat", self.files and self.regression["OLS"]["model"])
mt.add_cmd("bgod", self.files and self.regression["OLS"]["model"])
mt.add_cmd("bpag", self.files and self.regression["OLS"]["model"])
console.print(text=mt.menu_text, menu="Econometrics")
console.print()
def custom_reset(self):
"""Class specific component of reset command"""
return ["econometrics"]
def update_loaded(self):
self.list_dataset_cols = []
if not self.files:
self.loaded_dataset_cols = "\n"
self.list_dataset_cols.append("")
return
maxfile = max(len(file) for file in self.files)
self.loaded_dataset_cols = "\n"
for dataset, data in self.datasets.items():
max_files = (maxfile - len(dataset)) * " "
self.loaded_dataset_cols += (
f"\t{dataset} {max_files}: {', '.join(data.columns)}\n"
)
for col in data.columns:
self.list_dataset_cols.append(f"{dataset}.{col}")
@log_start_end(log=logger)
def call_load(self, other_args: List[str]):
"""Process load"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="load",
description="Load dataset (from previous export, custom imports or StatsModels).",
)
parser.add_argument(
"-f",
"--file",
help="File to load data in (can be custom import, "
"may have been exported before or can be from Statsmodels)",
type=str,
)
parser.add_argument(
"-a",
"--alias",
help="Alias name to give to the dataset",
type=str,
)
parser.add_argument(
"-e",
"--examples",
help="Use this argument to show examples of Statsmodels to load in. "
"See: https://www.statsmodels.org/devel/datasets/index.html",
action="store_true",
default=False,
dest="examples",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-f")
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
# show examples from statsmodels
if ns_parser.examples:
df = pd.DataFrame.from_dict(common_model.DATA_EXAMPLES, orient="index")
print_rich_table(
df,
headers=list(["description"]),
show_index=True,
index_name="file name",
title="Examples from Statsmodels",
)
return
if not ns_parser.file:
return
possible_data = list(common_model.DATA_EXAMPLES.keys()) + list(
self.DATA_FILES.keys()
)
if ns_parser.file not in possible_data:
file = ""
# Try to see if the user is just missing the extension
for file_ext in list(self.DATA_FILES.keys()):
if file_ext.startswith(ns_parser.file):
# found the correct file
file = file_ext
break
if not file:
console.print(
"[red]The file/dataset selected does not exist.[/red]"
)
return
else:
file = ns_parser.file
if ns_parser.alias:
alias = ns_parser.alias
else:
if "." in ns_parser.file:
alias = ".".join(ns_parser.file.split(".")[:-1])
else:
alias = ns_parser.file
# check if this dataset has been added already
if alias in self.files:
console.print(
"[red]The file/dataset selected has already been loaded.[/red]"
)
return
data = common_model.load(file, self.DATA_FILES, common_model.DATA_EXAMPLES)
if not data.empty:
data.columns = data.columns.map(lambda x: x.lower().replace(" ", "_"))
self.files.append(alias)
self.datasets[alias] = data
self.update_runtime_choices()
self.update_loaded()
@log_start_end(log=logger)
def call_export(self, other_args: List[str]):
"""Process export command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="export",
description="Export dataset to Excel",
)
parser.add_argument(
"-n",
"--name",
dest="name",
help="The name of the dataset you wish to export",
type=str,
)
parser.add_argument(
"-t",
"--type",
help="The file type you wish to export to",
dest="type",
choices=common_model.file_types,
type=str,
default="xlsx",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=NO_EXPORT
)
if ns_parser:
if not ns_parser.name or ns_parser.name not in self.datasets:
console.print("Please enter a valid dataset.")
else:
export_data(
ns_parser.type,
os.path.dirname(os.path.abspath(__file__)),
ns_parser.name,
self.datasets[ns_parser.name],
)
console.print()
@log_start_end(log=logger)
def call_remove(self, other_args: List[str]):
"""Process clear"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="remove",
description="Remove a dataset from the loaded dataset list",
)
parser.add_argument(
"-n",
"--name",
help="The name of the dataset you want to remove",
dest="name",
type=str,
choices=list(self.datasets.keys()),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if not ns_parser:
return
if not ns_parser.name:
console.print("Please enter a valid dataset.\n")
return
if ns_parser.name not in self.datasets:
console.print(f"[red]'{ns_parser.name}' is not a loaded dataset.[/red]\n")
return
del self.datasets[ns_parser.name]
self.files.remove(ns_parser.name)
self.update_runtime_choices()
self.update_loaded()
@log_start_end(log=logger)
def call_plot(self, other_args: List[str]):
"""Process plot command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="plot",
description="Plot data based on the index",
)
parser.add_argument(
"-v",
"--values",
help="Dataset.column values to be displayed in a plot",
dest="values",
type=check_list_values(self.choices["plot"]),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-v")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser and ns_parser.values:
data: Dict = {}
for datasetcol in ns_parser.values:
dataset, col = datasetcol.split(".")
data[datasetcol] = self.datasets[dataset][col]
econometrics_view.display_plot(
data,
ns_parser.export,
)
@log_start_end(log=logger)
def call_show(self, other_args: List[str]):
"""Process show command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="show",
description="Show a portion of the DataFrame",
)
parser.add_argument(
"-n",
"--name",
type=str,
choices=self.files,
dest="name",
help="The name of the database you want to show data for",
)
parser.add_argument(
"-s",
"--sortby",
help="Sort based on a column in the DataFrame",
type=str,
dest="sortby",
default="",
)
parser.add_argument(
"-r",
"--reverse",
action="store_true",
dest="reverse",
default=False,
help=(
"Data is sorted in descending order by default. "
"Reverse flag will sort it in an ascending way. "
"Only works when raw data is displayed."
),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED, limit=10
)
if ns_parser:
if not ns_parser.name:
dataset_names = list(self.datasets.keys())
else:
dataset_names = [ns_parser.name]
for name in dataset_names:
df = self.datasets[name]
if name in self.datasets and self.datasets[name].empty:
return console.print(
f"[red]No data available for {ns_parser.name}.[/red]\n"
)
if ns_parser.sortby:
sort_column = ns_parser.sortby
if sort_column not in self.datasets[name].columns:
console.print(
f"[red]{sort_column} not a valid column. Showing without sorting.\n[/red]"
)
else:
df = df.sort_values(by=sort_column, ascending=ns_parser.reverse)
print_rich_table(
df.head(ns_parser.limit),
headers=list(df.columns),
show_index=True,
title=f"Dataset {name} | Showing {ns_parser.limit} of {len(df)} rows",
)
export_data(
ns_parser.export,
os.path.dirname(os.path.abspath(__file__)),
f"{ns_parser.name}_show",
df.head(ns_parser.limit),
)
@log_start_end(log=logger)
def call_desc(self, other_args: List[str]):
"""Process desc command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="desc",
description="Show the descriptive statistics of the dataset",
)
parser.add_argument(
"-n",
"--name",
type=str,
choices=self.choices["desc"],
dest="name",
help="The name of the dataset.column you want to show the descriptive statistics",
required="-h" not in other_args,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if "." in ns_parser.name:
dataset, col = ns_parser.name.split(".")
df = self.datasets[dataset][col].describe()
print_rich_table(
df.to_frame(),
headers=[col],
show_index=True,
title=f"Statistics for dataset: '{dataset}'",
)
export_data(
ns_parser.export,
os.path.dirname(os.path.abspath(__file__)),
f"{dataset}_{col}_desc",
df,
)
else:
df = self.datasets[ns_parser.name]
if not df.empty:
df = df.describe()
print_rich_table(
df,
headers=self.datasets[ns_parser.name].columns,
show_index=True,
title=f"Statistics for dataset: '{ns_parser.name}'",
)
export_data(
ns_parser.export,
os.path.dirname(os.path.abspath(__file__)),
f"{ns_parser.name}_desc",
df,
)
else:
console.print("Empty dataset")
@log_start_end(log=logger)
def call_type(self, other_args: List[str]):
"""Process type"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="type",
description="Show the type of the columns of the dataset and/or change the type of the column",
)
parser.add_argument(
"-n",
"--name",
type=str,
dest="name",
help="Provide dataset.column series to change type or dataset to see types.",
choices=self.choices["type"],
)
parser.add_argument(
"--format",
type=str,
choices=self.DATA_TYPES,
dest="format",
help=(
"Set the format for the dataset.column defined. This can be: "
"date, int, float, str, bool or category"
),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
if ns_parser.name:
if "." in ns_parser.name:
dataset, column = ns_parser.name.split(".")
if ns_parser.format:
if ns_parser.format == "date":
self.datasets[dataset][column] = pd.to_datetime(
self.datasets[dataset][column].values,
)
else:
self.datasets[dataset][column] = self.datasets[dataset][
column
].astype(ns_parser.format)
console.print(
f"Update '{ns_parser.name}' with type '{ns_parser.format}'"
)
else:
console.print(
f"The type of '{ns_parser.name}' is '{self.datasets[dataset][column].dtypes}'"
)
else:
print_rich_table(
pd.DataFrame(self.datasets[ns_parser.name].dtypes),
headers=list(["dtype"]),
show_index=True,
index_name="column",
title=str(ns_parser.name),
)
else:
for dataset_name, data in self.datasets.items():
print_rich_table(
pd.DataFrame(data.dtypes),
headers=list(["dtype"]),
show_index=True,
index_name="column",
title=str(dataset_name),
)
console.print()
@log_start_end(log=logger)
def call_index(self, other_args: List[str]):
"""Process index"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="index",
description="Set a (multi) index for the dataset",
)
parser.add_argument(
"-n",
"--name",
type=str,
dest="name",
choices=list(self.datasets.keys()),
help="Name of dataset to select index from",
required="-h" not in other_args,
)
parser.add_argument(
"-i",
"--index",
type=str,
dest="index",
help="Columns from the dataset the user wishes to set as default",
default="",
)
parser.add_argument(
"-a",
"--adjustment",
help="Whether to allow for making adjustments to the dataset to align it with the use case for "
"Timeseries and Panel Data regressions",
dest="adjustment",
action="store_true",
default=False,
)
parser.add_argument(
"-d",
"--drop",
help="Whether to drop the column(s) the index is set for.",
dest="drop",
action="store_true",
default=False,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
name = ns_parser.name
index = ns_parser.index
if index:
if "," in index:
values_found = [val.strip() for val in index.split(",")]
else:
values_found = [index]
columns = list()
for value in values_found:
# check if the value is valid
if value in self.datasets[name].columns:
columns.append(value)
else:
console.print(f"[red]'{value}' is not valid.[/red]")
dataset = self.datasets[name]
if not pd.Index(np.arange(0, len(dataset))).equals(dataset.index):
console.print(
"As an index has been set, resetting the current index."
)
if dataset.index.name in dataset.columns:
dataset = dataset.reset_index(drop=True)
else:
dataset = dataset.reset_index(drop=False)
for column in columns:
if column not in dataset.columns:
console.print(
f"[red]The column '{column}' is not available in the dataset {name}."
f"Please choose one of the following: {', '.join(dataset.columns)}[/red]"
)
return
if ns_parser.adjustment:
if len(columns) > 1 and dataset[columns[0]].isnull().any():
null_values = dataset[dataset[columns[0]].isnull()]
console.print(
f"The column '{columns[0]}' contains {len(null_values)} NaN "
"values. As multiple columns are provided, it is assumed this "
"column represents entities (i), the NaN values are forward "
"filled. Remove the -a argument to disable this."
)
dataset[columns[0]] = dataset[columns[0]].fillna(method="ffill")
if dataset[columns[-1]].isnull().any():
# This checks whether NaT (missing values) exists within the DataFrame
null_values = dataset[dataset[columns[-1]].isnull()]
console.print(
f"The time index '{columns[-1]}' contains {len(null_values)} "
"NaNs which are removed from the dataset. Remove the -a argument to disable this."
)
dataset = dataset[dataset[columns[-1]].notnull()]
self.datasets[name] = dataset.set_index(columns, drop=ns_parser.drop)
console.print(
f"Successfully updated '{name}' index to be '{', '.join(columns)}'\n"
)
self.update_runtime_choices()
else:
print_rich_table(
self.datasets[name].head(3),
headers=list(self.datasets[name].columns),
show_index=True,
title=f"Dataset '{name}'",
)
@log_start_end(log=logger)
def call_clean(self, other_args: List[str]):
"""Process clean"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="clean",
description="Clean a dataset by filling and dropping NaN values.",
)
parser.add_argument(
"-n",
"--name",
help="The name of the dataset you want to clean up",
dest="name",
type=str,
choices=list(self.datasets.keys()),
)
parser.add_argument(
"--fill",
help="The method of filling NaNs. This has options to fill rows (rfill, rbfill, rffill) or fill "
"columns (cfill, cbfill, cffill). Furthermore, it has the option to forward fill and backward fill "
"(up to --limit) which refer to how many rows/columns can be set equal to the last non-NaN value",
dest="fill",
choices=["rfill", "cfill", "rbfill", "cbfill", "rffill", "bffill"],
default="",
)
parser.add_argument(
"-d",
"--drop",
help="The method of dropping NaNs. This either has the option rdrop (drop rows that contain NaNs) "
"or cdrop (drop columns that contain NaNs)",
dest="drop",
choices=["rdrop", "cdrop"],
default="",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, NO_EXPORT, limit=5
)
if ns_parser:
self.datasets[ns_parser.name] = econometrics_model.clean(
self.datasets[ns_parser.name],
ns_parser.fill,
ns_parser.drop,
ns_parser.limit,
)
console.print(f"Successfully cleaned '{ns_parser.name}' dataset")
console.print()
@log_start_end(log=logger)
def call_add(self, other_args: List[str]):
"""Process add"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="add",
description="Add columns to your dataframe with the option to use formulas. E.g."
" newdatasetcol = basedatasetcol sign criteriaordatasetcol"
" thesis.high_revenue = thesis.revenue > 1000"
" dataset.debt_ratio = dataset.debt div dataset2.assets",
)
parser.add_argument(
"-n",
"--newdatasetcol",
help="New dataset column to be added with format: dataset.column",
dest="newdatasetcol",
type=str,
required="-h" not in other_args,
)
parser.add_argument(
"-b",
"--basedatasetcol",
help="Base dataset column to be used as base with format: dataset.column",
dest="basedatasetcol",
type=str,
required="-h" not in other_args,
)
parser.add_argument(
"-s",
"--sign",
help="Sign to be applied to the base dataset column",
dest="sign",
choices=list(self.signs.keys()) + [">", "<", ">=", "<=", "=="],
required="-h" not in other_args,
type=str,
)
parser.add_argument(
"-c",
"--criteriaordatasetcol",
help="Either dataset column to be applied on top of base dataset or criteria",
dest="criteriaordatasetcol",
required="-h" not in other_args,
type=str,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-n")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
dataset, new_column = ns_parser.newdatasetcol.split(".")
dataset2, existing_column = ns_parser.basedatasetcol.split(".")
for sign, operator in self.signs.items():
if sign == ns_parser.sign:
ns_parser.sign = operator
if dataset not in self.datasets:
console.print(
f"Not able to find the dataset {dataset}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif dataset2 not in self.datasets:
console.print(
f"Not able to find the dataset {dataset2}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif existing_column not in self.datasets[dataset2]:
console.print(
f"Not able to find the column {existing_column}. Please choose one of "
f"the following: {', '.join(self.datasets[dataset2].columns)}"
)
elif len(ns_parser.criteriaordatasetcol.split(".")) > 1:
dataset3, existing_column2 = ns_parser.criteriaordatasetcol.split(".")
if dataset3 not in self.datasets:
console.print(
f"Not able to find the dataset {dataset3}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif existing_column2 not in self.datasets[dataset3]:
console.print(
f"Not able to find the column {existing_column2}. Please choose one of "
f"the following: {', '.join(self.datasets[dataset3].columns)}"
)
else:
pd.eval(
f"{new_column} = self.datasets[dataset2][existing_column] "
f"{ns_parser.sign} self.datasets[dataset3][existing_column2]",
target=self.datasets[dataset],
inplace=True,
)
else:
pd.eval(
f"{new_column} = self.datasets[dataset2][existing_column] "
f"{ns_parser.sign} {ns_parser.criteriaordatasetcol}",
target=self.datasets[dataset],
inplace=True,
)
self.update_runtime_choices()
self.update_loaded()
console.print()
@log_start_end(log=logger)
def call_delete(self, other_args: List[str]):
"""Process add"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="delete",
description="The column you want to delete from a dataset.",
)
parser.add_argument(
"-d",
"--delete",
help="The columns you want to delete from a dataset. Use format: <dataset.column> or"
" multiple with <dataset.column>,<dataset.column2>",
dest="delete",
type=check_list_values(self.choices["delete"]),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
for option in ns_parser.delete:
dataset, column = option.split(".")
if dataset not in self.datasets:
console.print(
f"Not able to find the dataset {dataset}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif column not in self.datasets[dataset]:
console.print(
f"Not able to find the column {column}. Please choose one of "
f"the following: {', '.join(self.datasets[dataset].columns)}"
)
else:
del self.datasets[dataset][column]
self.update_runtime_choices()
console.print()
@log_start_end(log=logger)
def call_combine(self, other_args: List[str]):
"""Process combine"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="combine",
description="The columns you want to add to a dataset. The first argument is the dataset to add columns in"
"and the remaining could be: <datasetX.column2>,<datasetY.column3>",
)
parser.add_argument(
"-d",
"--dataset",
help="Dataset to add columns to",
dest="dataset",
choices=self.choices["combine"],
)
parser.add_argument(
"-c",
"--columns",
help="The columns we want to add <dataset.column>,<dataset.column2>",
dest="columns",
type=check_list_values(self.choices["delete"]),
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
if ns_parser.dataset not in self.datasets:
console.print(
f"Not able to find the dataset {ns_parser.dataset}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
return
data = self.datasets[ns_parser.dataset]
for option in ns_parser.columns:
dataset, column = option.split(".")
if dataset not in self.datasets:
console.print(
f"Not able to find the dataset {dataset}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif column not in self.datasets[dataset]:
console.print(
f"Not able to find the column {column}. Please choose one of "
f"the following: {', '.join(self.datasets[dataset].columns)}"
)
else:
data[f"{dataset}_{column}"] = self.datasets[dataset][column]
self.update_runtime_choices()
console.print()
@log_start_end(log=logger)
def call_rename(self, other_args: List[str]):
"""Process rename"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rename",
description="The column you want to rename from a dataset.",
)
parser.add_argument(
"-d",
"--dataset",
help="Dataset that will get a column renamed",
dest="dataset",
choices=self.choices["rename"],
type=str,
)
parser.add_argument(
"-o",
"--oldcol",
help="Old column from dataset to be renamed",
dest="oldcol",
type=str,
required="-h" not in other_args,
)
parser.add_argument(
"-n",
"--newcol",
help="New column from dataset to be renamed",
dest="newcol",
type=str,
required="-h" not in other_args,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = self.parse_known_args_and_warn(parser, other_args, NO_EXPORT)
if ns_parser:
dataset = ns_parser.dataset
column_old = ns_parser.oldcol
column_new = ns_parser.newcol
if dataset not in self.datasets:
console.print(
f"Not able to find the dataset {dataset}. Please choose one of "
f"the following: {', '.join(self.datasets)}"
)
elif column_old not in self.datasets[dataset]:
console.print(
f"Not able to find the column {column_old}. Please choose one of "
f"the following: {', '.join(self.datasets[dataset].columns)}"
)
else:
self.datasets[dataset] = self.datasets[dataset].rename(
columns={column_old: column_new}
)
self.update_runtime_choices()
console.print()
@log_start_end(log=logger)
def call_ols(self, other_args: List[str]):
"""Process ols command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="ols",
description="Performs an OLS regression on timeseries data.",
)
parser.add_argument(
"-d",
"--dependent",
type=str,
dest="dependent",
help="The dependent variable on the regression you would like to perform",
required="-h" not in other_args,
)
parser.add_argument(
"-i",
"--independent",
type=check_list_values(self.choices["regressions"]),
dest="independent",
help=(
"The independent variables on the regression you would like to perform. "
"E.g. historical.high,historical.low"
),
required="-h" not in other_args,
)
parser.add_argument(
"--no-output",
action="store_true",
default=False,
help="Hide the output of the regression",
dest="no_output",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if "," in ns_parser.dependent:
console.print(
"It appears you have selected multiple variables for the dependent variable. "
"Please select one.\nDid you intend to include these variables as independent "
f"variables? Use -i {ns_parser.dependent} in this case.\n"
)
elif ns_parser.dependent in self.choices["regressions"]:
(
regression_df,
dependent_variable,
independent_variables,
) = regression_model.get_regression_data(
[ns_parser.dependent] + ns_parser.independent,
self.datasets,
"OLS",
)
self.regression["OLS"]["data"] = regression_df
self.regression["OLS"]["dependent"] = dependent_variable
self.regression["OLS"]["independent"] = independent_variables
model = regression_model.get_ols(
regression_df[dependent_variable],
regression_df[independent_variables],
)
self.regression["OLS"]["model"] = model
if not ns_parser.no_output:
console.print(model.summary())
else:
console.print(
f"{ns_parser.dependent} not in {','.join(self.choices['regressions'])}\n"
f"Please choose a valid dataset and column combination.\n"
)
@log_start_end(log=logger)
def call_norm(self, other_args: List[str]):
"""Process normality command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="norm",
description="Test whether the used data is normally distributed.",
)
parser.add_argument(
"-v",
"--value",
type=str,
choices=self.choices["norm"],
dest="column",
help="The dataset.column you want to test normality for",
required="-h" not in other_args,
)
parser.add_argument(
"-p",
"--plot",
dest="plot",
help="Whether you wish to plot a histogram to visually depict normality",
action="store_true",
default=False,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-v")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser and ns_parser.column:
dataset, column = ns_parser.column.split(".")
if isinstance(self.datasets[dataset][column].index, pd.MultiIndex):
return console.print(
f"The column '{column}' in '{dataset}' is a MultiIndex. To test for normality"
", make sure to set a singular time index.\n"
)
if dataset in self.datasets:
if isinstance(self.datasets[dataset], pd.Series):
data = self.datasets[dataset]
elif isinstance(self.datasets[dataset], pd.DataFrame):
data = self.datasets[dataset][column]
else:
return console.print(
f"The type of {dataset} ({type(dataset)} is not an option."
)
else:
return console.print(f"Can not find {dataset}. Did you load the data?")
econometrics_view.display_norm(
data, dataset, column, ns_parser.plot, ns_parser.export
)
@log_start_end(log=logger)
def call_root(self, other_args: List[str]):
"""Process unit root command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="root",
description="Show unit root tests of a column of a dataset",
)
parser.add_argument(
"-v",
"--value",
type=str,
choices=self.choices["root"],
dest="column",
help="The column and name of the database you want test unit root for",
required="-h" not in other_args,
)
parser.add_argument(
"-r",
"--fuller_reg",
help="Type of regression. Can be 'c','ct','ctt','nc'. c - Constant and t - trend order",
choices=["c", "ct", "ctt", "n"],
default="c",
type=str,
dest="fuller_reg",
)
parser.add_argument(
"-k",
"--kps_reg",
help="Type of regression. Can be 'c', 'ct'. c - Constant and t - trend order",
choices=["c", "ct"],
type=str,
dest="kpss_reg",
default="c",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-v")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser and ns_parser.column:
if "." in ns_parser.column:
dataset, column = ns_parser.column.split(".")
else:
console.print(
"[red]Column must be formatted as 'dataset.column'[/red]\n"
)
if isinstance(self.datasets[dataset][column].index, pd.MultiIndex):
console.print(
f"The column '{column}' from the dataset '{dataset}' is a MultiIndex. To test for unitroot in a "
"timeseries, make sure to set a singular time index.\n"
)
else:
if isinstance(self.datasets[dataset], pd.Series):
data = self.datasets[dataset]
elif isinstance(self.datasets[dataset], pd.DataFrame):
data = self.datasets[dataset][column]
else:
return console.print(
"Can not select data due to the data not being a DataFrame or Series."
)
econometrics_view.display_root(
data,
dataset,
column,
ns_parser.fuller_reg,
ns_parser.kpss_reg,
ns_parser.export,
)
@log_start_end(log=logger)
def call_panel(self, other_args: List[str]):
"""Process panel command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="panel",
description="Performs regression analysis on Panel Data. There are a multitude of options to select "
"from to fit the needs of restrictions of the dataset.",
)
parser.add_argument(
"-d",
"--dependent",
type=str,
dest="dependent",
help="The dependent variable on the regression you would like to perform",
required="-h" not in other_args,
)
parser.add_argument(
"-i",
"--independent",
type=check_list_values(self.choices["regressions"]),
dest="independent",
help=(
"The independent variables on the regression you would like to perform. "
"E.g. wage_panel.married,wage_panel.union"
),
required="-h" not in other_args,
)
parser.add_argument(
"-r",
"--regression",
type=str,
choices=self.PANEL_CHOICES,
dest="type",
help="The type of regression you wish to perform. This can be either pols (Pooled OLS), "
"re (Random Effects), bols (Between OLS), fe (Fixed Effects) or fdols (First Difference OLS)",
default="pols",
)
parser.add_argument(
"-e",
"--entity_effects",
dest="entity_effects",
help="Using this command creates entity effects, which is equivalent to including dummies for each entity. "
"This is only used within Fixed Effects estimations (when type is set to 'fe')",
action="store_true",
default=False,
)
parser.add_argument(
"-t",
"--time_effects",
dest="time_effects",
help="Using this command creates time effects, which is equivalent to including dummies for each time. "
"This is only used within Fixed Effects estimations (when type is set to 'fe')",
action="store_true",
default=False,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if "," in ns_parser.dependent:
console.print(
"It appears you have selected multiple variables for the dependent variable. "
"The model only accepts one.\nDid you intend to include these variables as independent "
f"variables? Use -i {ns_parser.dependent} in this case.\n"
)
elif ns_parser.dependent in self.choices["regressions"]:
regression_vars = [ns_parser.dependent] + ns_parser.independent
if regression_vars and len(regression_vars) > 1:
for variable in regression_vars:
if "." not in variable:
console.print(
"[red]Please follow the format 'dataset.column'[/red]\n"
)
continue
dataset, column = variable.split(".")
if not isinstance(
self.datasets[dataset][column].index, pd.MultiIndex
):
other_column = (
self.datasets[dataset].drop(column, axis=1).columns[0]
)
return console.print(
f"The column '{column}' from the dataset '{dataset}' is not a MultiIndex. Make sure "
f"you set the index correctly with the index (e.g. index {dataset} -i {column},"
f"{other_column}) command where the first level is the entity (e.g. Tesla Inc.) and "
f"the second level the date (e.g. 2021-03-31)\n"
)
# Ensure that OLS is always ran to be able to perform tests
regression_types = [ns_parser.type.upper(), "OLS"]
for regression in regression_types:
regression_name = regression
if regression == "FE":
if ns_parser.entity_effects:
regression_name = regression_name + "_EE"
if ns_parser.time_effects:
regression_name = regression_name + "_IE"
(
regression_df,
dependent_variable,
independent_variables,
) = regression_model.get_regression_data(
[ns_parser.dependent] + ns_parser.independent,
self.datasets,
regression,
)
self.regression[regression]["data"] = regression_df
self.regression[regression]["dependent"] = dependent_variable
self.regression[regression][
"independent"
] = independent_variables
self.regression[regression_name][
"model"
] = regression_view.display_panel(
regression_df[dependent_variable],
regression_df[independent_variables],
regression,
ns_parser.entity_effects,
ns_parser.time_effects,
ns_parser.export,
)
else:
console.print(
f"{ns_parser.dependent} not in {','.join(self.choices['regressions'])}\n"
f"Please choose a valid dataset and column combination.\n"
)
@log_start_end(log=logger)
def call_compare(self, other_args: List[str]):
"""Process compare command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="compare",
description="Compare results between all activated Panel regression models",
)
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
regression_model.get_comparison(self.regression, ns_parser.export)
console.print()
@log_start_end(log=logger)
def call_dwat(self, other_args: List[str]):
"""Process unitroot command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="dwat",
description=(
"Show autocorrelation tests from Durbin-Watson. "
"Needs OLS to be run in advance with independent and dependent variables"
),
)
parser.add_argument(
"-p",
"--plot",
help="Plot the residuals",
dest="plot",
action="store_true",
default=False,
)
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if not self.regression["OLS"]["model"]:
console.print(
"Please perform an OLS regression before estimating the Durbin-Watson statistic.\n"
)
else:
dependent_variable = self.regression["OLS"]["data"][
self.regression["OLS"]["dependent"]
]
regression_view.display_dwat(
self.regression["OLS"]["model"],
dependent_variable,
ns_parser.plot,
ns_parser.export,
)
@log_start_end(log=logger)
def call_bgod(self, other_args):
"""Process bgod command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="bgod",
description=(
"Show Breusch-Godfrey autocorrelation test results."
"Needs OLS to be run in advance with independent and dependent variables"
),
)
parser.add_argument(
"-l",
"--lags",
type=check_positive,
dest="lags",
help="The lags for the Breusch-Godfrey test",
default=3,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if not self.regression["OLS"]["model"]:
console.print(
"Perform an OLS regression before estimating the Breusch-Godfrey statistic.\n"
)
else:
regression_view.display_bgod(
self.regression["OLS"]["model"], ns_parser.lags, ns_parser.export
)
@log_start_end(log=logger)
def call_bpag(self, other_args):
"""Process bpag command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="bpag",
description=(
"Show Breusch-Pagan heteroscedasticity test results. "
"Needs OLS to be run in advance with independent and dependent variables"
),
)
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
if not self.regression["OLS"]["model"]:
console.print(
"Perform an OLS regression before estimating the Breusch-Pagan statistic.\n"
)
else:
regression_view.display_bpag(
self.regression["OLS"]["model"], ns_parser.export
)
@log_start_end(log=logger)
def call_granger(self, other_args: List[str]):
"""Process granger command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="granger",
description="Show Granger causality between two timeseries",
)
parser.add_argument(
"-t",
"--timeseries",
choices=self.choices["granger"],
help="Requires two time series, the first time series is assumed to be Granger-caused "
"by the second time series.",
type=str,
dest="ts",
metavar="Available time series",
)
parser.add_argument(
"-l",
"--lags",
help="How many lags should be included",
type=int,
dest="lags",
default=3,
)
parser.add_argument(
"-c",
"--confidence",
help="Set the confidence level",
type=check_positive_float,
dest="confidence",
default=0.05,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-t")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser and ns_parser.ts:
datasetcol_y, datasetcol_x = ns_parser.ts.split(",")
dataset_y, column_y = datasetcol_y.split(".")
dataset_x, column_x = datasetcol_x.split(".")
econometrics_view.display_granger(
self.datasets[dataset_y][column_y].rename(datasetcol_y),
self.datasets[dataset_x][column_x].rename(datasetcol_x),
ns_parser.lags,
ns_parser.confidence,
ns_parser.export,
)
@log_start_end(log=logger)
def call_coint(self, other_args: List[str]):
"""Process coint command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="coint",
description="Show co-integration between two timeseries",
)
parser.add_argument(
"-t",
"--time_series",
help="The time series you wish to test co-integration on. E.g. historical.open,historical2.close.",
dest="ts",
type=check_list_values(self.choices["coint"]),
required="-h" not in other_args,
)
parser.add_argument(
"-p",
"--plot",
help="Plot Z-Values",
dest="plot",
action="store_true",
default=False,
)
parser.add_argument(
"-s",
"--significant",
help="Show only companies that have p-values lower than this percentage",
dest="significant",
type=float,
default=0,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-t")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser:
if ns_parser.ts:
# We are going to pass through a variable number of series, so datasets will be a list of series
if len(ns_parser.ts) > 1:
datasets = []
for series in ns_parser.ts:
if "." not in series:
console.print(
"[red]Invalid time series format. Should be dataset.column, "
"e.g. historical.open[/red]\n"
)
else:
dataset, column = series.split(".")
datasets.append(self.datasets[dataset][column])
econometrics_view.display_cointegration_test(
*datasets,
significant=ns_parser.significant,
plot=ns_parser.plot,
export=ns_parser.export,
)
else:
console.print(
"[red]More than one dataset.column must be provided.\n[/red]"
)