diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9a1a62fd675..17f52d31bea 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -26,7 +26,7 @@ jobs:
- run: pip install bandit black codespell flake8 mypy pyupgrade safety pylint
- run: bandit -r . || true
- run: black --check .
- - run: codespell --ignore-words-list="mape","ba" --quiet-level=2 --skip=".git" --skip="*.yaml" --skip="*.json"
+ - run: codespell --ignore-words-list="mape","ba","hist" --quiet-level=2 --skip=".git" --skip="*.yaml" --skip="*.json"
- run: flake8 . --count --ignore=E203,W503 --max-line-length=122 --show-source --statistics
- run: mypy --ignore-missing-imports .
- run: shopt -s globstar && pyupgrade --py36-plus **/*.py
diff --git a/ROADMAP.md b/ROADMAP.md
index d6ed49eae3d..7cf2d4656f8 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -139,8 +139,8 @@
### Short term
-* [x] Summary statistics, cdf, histogram, box-whiskers, cusum, decompose (@didier) - [PR #289](https://github.com/DidierRLopes/GamestonkTerminal/pull/289)
-* [ ] Residuals sub-menu with Hypothesis tests (e.g. Kurtosis, Jarques-Bera, ARCH, ADF, ... and such) (@didier)
+* [x] Summary stats, CDF, Histogram, Box Whiskers, Decompose (in Trend, Cyclic Trend, Season), CUSUM (@didier) - [PR #289](https://github.com/DidierRLopes/GamestonkTerminal/pull/289)
+* [ ] Hypothesis tests (e.g. Kurtosis, Jarques-Bera, ARCH, ADF, ...) (@didier)
### Long term
diff --git a/gamestonk_terminal/exploratory_data_analysis/README.md b/gamestonk_terminal/exploratory_data_analysis/README.md
new file mode 100644
index 00000000000..244d1079d4b
--- /dev/null
+++ b/gamestonk_terminal/exploratory_data_analysis/README.md
@@ -0,0 +1,136 @@
+# EXPLORATORY DATA ANALYSIS
+
+This menu aims to perform exploratory data analysis on a pre-loaded stock, and the usage of the following commands along with an example will be exploited below.
+
+* [summary](#summary)
+ * brief summary statistics
+* [hist](#hist)
+ * histogram with density plot
+* [cdf](#cdf)
+ * cumulative distribution function
+* [bwy](#bwy)
+ * box and whisker yearly plot
+* [bwm](#bwm)
+ * box and whisker monthly plot
+* [rolling](#rolling)
+ * rolling mean and std deviation
+* [decompose](#decompose)
+ * decomposition in cyclic-trend, season, and residuald
+* [cusum](#cusum)
+ * detects abrupt changes using cumulative sum algorithm
+* [acf](#acf)
+ * (partial) auto-correlation function differentials
+
+
+## summary
+
+```text
+usage: summary
+```
+
+Summary statistics
+
+
+
+
+## hist
+
+```text
+usage: hist
+```
+
+Plot histogram and density
+
+
+
+
+## cdf
+
+```text
+usage: cdf
+```
+
+Plot cumulative distribution function
+
+
+
+
+## bwy
+
+```text
+usage: bwy
+```
+
+Box and Whisker plot yearly
+
+
+
+
+## bwm
+
+```text
+usage: bwm
+```
+
+Box and Whisker plot monthly
+
+
+
+
+## rolling
+
+```text
+usage: rolling [-w ROLLING_WINDOW]
+```
+
+Rolling mean and std deviation
+
+* -w : rolling window. Default 100.
+
+
+
+
+## decompose
+
+```text
+usage: decompose [-m]
+```
+
+Decompose time series as:
+ - Additive Time Series = Level + CyclicTrend + Residual + Seasonality
+ - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality
+
+* -m : multiplicative model. Default additive.
+
+
+
+
+
+
+## cusum
+
+```text
+usage: cusum [-t THRESHOLD] [-d DRIFT]
+```
+
+Cumulative sum algorithm (CUSUM) to detect abrupt changes in data
+
+* -t : threshold. Default (MAX-min)/40.
+* -d : drift. Default (MAX-min)/80.
+
+
+
+
+## acf
+
+```text
+usage: acf [-l LAGS]
+```
+
+Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data
+
+* -l : maximum lags to display in plots. Default 40.
+
+
+
+
diff --git a/gamestonk_terminal/exploratory_data_analysis/eda_api.py b/gamestonk_terminal/exploratory_data_analysis/eda_api.py
new file mode 100644
index 00000000000..b00784c64af
--- /dev/null
+++ b/gamestonk_terminal/exploratory_data_analysis/eda_api.py
@@ -0,0 +1,709 @@
+""" Exploratory Data Analysis API """
+__docformat__ = "numpy"
+
+import argparse
+from typing import List
+from detecta import detect_cusum
+from matplotlib import pyplot as plt
+from datetime import datetime
+import pandas as pd
+from pandas.plotting import register_matplotlib_converters
+import numpy as np
+import seaborn as sns
+import statsmodels.api as sm
+import matplotlib.gridspec as gridspec
+from statsmodels.tsa.seasonal import seasonal_decompose
+from gamestonk_terminal.helper_funcs import (
+ parse_known_args_and_warn,
+ plot_autoscale,
+ check_positive,
+)
+from gamestonk_terminal.config_plot import PLOT_DPI
+from gamestonk_terminal import feature_flags as gtff
+
+
+register_matplotlib_converters()
+
+
+def summary(other_args: List[str], stock: pd.DataFrame):
+ """Print summary statistics
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="summary",
+ description="""
+ Summary statistics
+ """,
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ df_stats = stock.describe(percentiles=[0.1, 0.25, 0.5, 0.75, 0.9])
+ df_stats.loc["var"] = df_stats.loc["std"] ** 2
+ print(df_stats.round(2).to_string())
+
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def hist(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime):
+ """Plot histogram and density
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="hist",
+ description="""
+ Histogram with depicted density and rug
+ """,
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
+ stock = stock["5. adjusted close"]
+
+ sns.distplot(
+ stock,
+ bins=35,
+ color="blue",
+ hist=True,
+ hist_kws={"edgecolor": "black"},
+ kde=True,
+ kde_kws={"color": "black", "lw": 3, "label": "KDE"},
+ rug=True,
+ rug_kws={"edgecolor": "orange"},
+ )
+ plt.title(
+ f"Histogram with Density of {ticker} from {start.strftime('%Y-%m-%d')}"
+ )
+ plt.ylabel("Density")
+ plt.xlabel("Share Price")
+ plt.grid(True)
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def cdf(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime):
+ """Plot cumulative distribution function
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="cdf",
+ description="""
+ Cumulative distribution function
+ """,
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
+ stock = stock["5. adjusted close"]
+
+ cdf = stock.value_counts().sort_index().div(len(stock)).cumsum()
+ cdf.plot(lw=2)
+ plt.title(
+ f"Cumulative Distribution Function of {ticker} from {start.strftime('%Y-%m-%d')}"
+ )
+ plt.ylabel("Probability")
+ plt.xlabel("Share Price")
+ minVal = stock.values.min()
+ q25 = np.quantile(stock.values, 0.25)
+ medianVal = np.quantile(stock.values, 0.5)
+ q75 = np.quantile(stock.values, 0.75)
+ data = [
+ (minVal, q25),
+ (0.25, 0.25),
+ "r",
+ (q25, q25),
+ (0, 0.25),
+ "r",
+ (minVal, medianVal),
+ (0.5, 0.5),
+ "r",
+ (medianVal, medianVal),
+ (0, 0.5),
+ "r",
+ (minVal, q75),
+ (0.75, 0.75),
+ "r",
+ (q75, q75),
+ (0, 0.75),
+ "r",
+ ]
+ plt.plot(*data, ls="--")
+ plt.text(minVal + (q25 - minVal) / 2, 0.27, "Q1", color="r", fontweight="bold")
+ plt.text(
+ minVal + (medianVal - minVal) / 2,
+ 0.52,
+ "Median",
+ color="r",
+ fontweight="bold",
+ )
+ plt.text(minVal + (q75 - minVal) / 2, 0.77, "Q3", color="r", fontweight="bold")
+ plt.xlim(cdf.index[0], cdf.index[-1])
+ plt.grid(True)
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def bwy(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime):
+ """Box and Whisker plot yearly
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="bwy",
+ description="""
+ Box and Whisker plot yearly
+ """,
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
+ stock = stock["5. adjusted close"]
+
+ sns.set(style="whitegrid")
+ box_plot = sns.boxplot(x=stock.index.year, y=stock)
+ box_plot.set(
+ xlabel="Year",
+ ylabel="Share Price",
+ title=f"Box-plot per Year of {ticker} from {start.strftime('%Y-%m-%d')}",
+ )
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def bwm(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime):
+ """Box and Whisker plot monthly
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="bwm",
+ description="""
+ Box and Whisker plot monthly
+ """,
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
+ stock = stock["5. adjusted close"]
+
+ sns.set(style="whitegrid")
+ box_plot = sns.boxplot(x=stock.index.month, y=stock)
+ box_plot.set(
+ xlabel="Month",
+ ylabel="Share Price",
+ title=f"Box-plot per Month of {ticker} from {start.strftime('%Y-%m-%d')}",
+ )
+ l_months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ]
+ l_ticks = list()
+ for val in box_plot.get_xticklabels():
+ l_ticks.append(l_months[int(val.get_text()) - 1])
+ box_plot.set_xticklabels(l_ticks)
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def rolling(other_args: List[str], ticker: str, stock: pd.DataFrame):
+ """Rolling mean and std deviation
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="rolling",
+ description="""
+ Rolling mean and std deviation
+ """,
+ )
+ parser.add_argument(
+ "-w",
+ "--window",
+ dest="rolling_window",
+ type=check_positive,
+ default=100,
+ help="rolling window",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ stock = stock["5. adjusted close"]
+
+ rolling_mean = stock.rolling(
+ ns_parser.rolling_window, center=True, min_periods=1
+ ).mean()
+ rolling_std = stock.rolling(
+ ns_parser.rolling_window, center=True, min_periods=1
+ ).std()
+
+ fig, axMean = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
+ axMean.plot(stock.index, stock.values, label=ticker, linewidth=2, color="black")
+ axMean.plot(rolling_mean, linestyle="--", linewidth=3, color="blue")
+ axMean.set_xlabel("Time")
+ axMean.set_ylabel("Share Price", color="blue")
+ axMean.legend(["Real values", "Rolling Mean"], loc=2)
+ axMean.tick_params(axis="y", labelcolor="blue")
+ axStd = axMean.twinx()
+ axStd.plot(
+ rolling_std, label="Rolling std", linestyle="--", color="green", linewidth=3
+ )
+ axStd.set_ylabel("Std Deviation")
+ axStd.legend(["Rolling std"], loc=1)
+ axStd.set_ylabel("Share Price standard deviation", color="green")
+ axStd.tick_params(axis="y", labelcolor="green")
+ axMean.set_title(
+ "Rolling mean and std with window "
+ + str(ns_parser.rolling_window)
+ + " applied to "
+ + ticker
+ )
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.grid(b=True, which="major", color="#666666", linestyle="-")
+ plt.minorticks_on()
+ plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2)
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def decompose(other_args: List[str], ticker: str, stock: pd.DataFrame):
+ """Decompose time series as:
+ - Additive Time Series = Level + CyclicTrend + Residual + Seasonality
+ - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="decompose",
+ description="""
+ Decompose time series as:
+ - Additive Time Series = Level + CyclicTrend + Residual + Seasonality
+ - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality
+ """,
+ )
+ parser.add_argument(
+ "-m",
+ "--multiplicative",
+ action="store_true",
+ default=False,
+ dest="multiplicative",
+ help="decompose using multiplicative model instead of additive",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ stock = stock["5. adjusted close"]
+
+ seasonal_periods = 5
+ # Hodrick-Prescott filter
+ # See Ravn and Uhlig: http://home.uchicago.edu/~huhlig/papers/uhlig.ravn.res.2002.pdf
+ lamb = 107360000000
+
+ fig = plt.figure(
+ figsize=plot_autoscale(), dpi=PLOT_DPI, constrained_layout=True
+ )
+ spec = gridspec.GridSpec(ncols=4, nrows=5, figure=fig)
+
+ fig.add_subplot(spec[0, :])
+ plt.plot(stock)
+
+ plt.title(ticker + " (Time-Series)")
+
+ if ns_parser.multiplicative:
+ resultMul = seasonal_decompose(
+ stock, model="multiplicative", period=seasonal_periods
+ )
+ cycleMul, trendMul = sm.tsa.filters.hpfilter(
+ resultMul.trend[resultMul.trend.notna().values], lamb=lamb
+ )
+
+ # Multiplicative model
+ fig.add_subplot(spec[1, :4])
+ plt.plot(resultMul.trend, lw=2, c="purple")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Multiplicative Cyclic-Trend")
+
+ fig.add_subplot(spec[2, 0:2])
+ plt.plot(trendMul, lw=2, c="tab:blue")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Multiplicative Trend component")
+
+ fig.add_subplot(spec[2, 2:])
+ plt.plot(cycleMul, lw=2, c="green")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Multiplicative Cycle component")
+
+ fig.add_subplot(spec[3, :])
+ plt.plot(resultMul.seasonal, lw=2, c="orange")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Multiplicative Seasonal effect")
+
+ fig.add_subplot(spec[4, :])
+ plt.plot(resultMul.resid, lw=2, c="red")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Multiplicative Residuals")
+
+ else:
+ resultAdd = seasonal_decompose(
+ stock, model="additive", period=seasonal_periods
+ )
+ cycleAdd, trendAdd = sm.tsa.filters.hpfilter(
+ resultAdd.trend[resultAdd.trend.notna().values], lamb=lamb
+ )
+
+ # Additive model
+ fig.add_subplot(spec[1, :4])
+ plt.plot(resultAdd.trend, lw=2, c="purple")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Additive Cyclic-Trend")
+
+ fig.add_subplot(spec[2, 0:2])
+ plt.plot(trendAdd, lw=2, c="tab:blue")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Additive Trend component")
+
+ fig.add_subplot(spec[2, 2:])
+ plt.plot(cycleAdd, lw=2, c="green")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Additive Cycle component")
+
+ fig.add_subplot(spec[3, :])
+ plt.plot(resultAdd.seasonal, lw=2, c="orange")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Additive Seasonal effect")
+
+ fig.add_subplot(spec[4, :])
+ plt.plot(resultAdd.resid, lw=2, c="red")
+ plt.xlim([stock.index[0], stock.index[-1]])
+ plt.title("Additive Residuals")
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ # From # https://otexts.com/fpp2/seasonal-strength.html
+
+ print("Time-Series Level is " + str(round(stock.mean(), 2)))
+
+ if ns_parser.multiplicative:
+ FtMul = max(0, 1 - np.var(resultMul.resid)) / np.var(
+ resultMul.trend + resultMul.resid
+ )
+ print("Strength of Trend: %.4f" % FtMul)
+ FsMul = max(
+ 0,
+ 1
+ - np.var(resultMul.resid)
+ / np.var(resultMul.seasonal + resultMul.resid),
+ )
+ print("Strength of Seasonality: %.4f" % FsMul)
+
+ else:
+ FtAdd = max(
+ 0,
+ 1 - np.var(resultAdd.resid) / np.var(resultAdd.trend + resultAdd.resid),
+ )
+ print("Strength of Trend: %.4f" % FtAdd)
+ FsAdd = max(
+ 0,
+ 1
+ - np.var(resultAdd.resid)
+ / np.var(resultAdd.seasonal + resultAdd.resid),
+ )
+ print("Strength of Seasonality: %.4f" % FsAdd)
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def cusum(other_args: List[str], ticker: str, stock: pd.DataFrame):
+ """Cumulative sum algorithm (CUSUM) to detect abrupt changes in data
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="cusum",
+ description="""
+ Cumulative sum algorithm (CUSUM) to detect abrupt changes in data
+ """,
+ )
+ parser.add_argument(
+ "-t",
+ "--threshold",
+ dest="threshold",
+ type=float,
+ default=(
+ max(stock["5. adjusted close"].values)
+ - min(stock["5. adjusted close"].values)
+ )
+ / 40,
+ help="threshold",
+ )
+ parser.add_argument(
+ "-d",
+ "--drift",
+ dest="drift",
+ type=float,
+ default=(
+ max(stock["5. adjusted close"].values)
+ - min(stock["5. adjusted close"].values)
+ )
+ / 80,
+ help="drift",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ stock = stock["5. adjusted close"]
+
+ detect_cusum(stock.values, ns_parser.threshold, ns_parser.drift, True, True)
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
+
+
+def acf(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime):
+ """Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data
+
+ Parameters
+ ----------
+ other_args : str
+ Command line arguments to be processed with argparse
+ ticker : str
+ Ticker of the stock
+ stock : pd.DataFrame
+ Stock data
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="acf",
+ description="""
+ Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data
+ """,
+ )
+ parser.add_argument(
+ "-l",
+ "--lags",
+ dest="lags",
+ type=check_positive,
+ default=40,
+ help="maximum lags to display in plots",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ stock = stock["5. adjusted close"]
+
+ fig = plt.figure(
+ figsize=plot_autoscale(), dpi=PLOT_DPI, constrained_layout=True
+ )
+ spec = gridspec.GridSpec(ncols=2, nrows=2, figure=fig)
+
+ # Diff Auto-correlation function for original time series
+ ax_acf = fig.add_subplot(spec[0, 0])
+ sm.graphics.tsa.plot_acf(
+ np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_acf
+ )
+ plt.title(f"Diff({ticker}) Auto-Correlation from {start.strftime('%Y-%m-%d')}")
+ # Diff Partial auto-correlation function for original time series
+ ax_pacf = fig.add_subplot(spec[0, 1])
+ sm.graphics.tsa.plot_pacf(
+ np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_pacf
+ )
+ plt.title(
+ f"Diff({ticker}) Partial Auto-Correlation from {start.strftime('%Y-%m-%d')}"
+ )
+
+ # Diff Diff Auto-correlation function for original time series
+ ax_acf = fig.add_subplot(spec[1, 0])
+ sm.graphics.tsa.plot_acf(
+ np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_acf
+ )
+ plt.title(
+ f"Diff(Diff({ticker})) Auto-Correlation from {start.strftime('%Y-%m-%d')}"
+ )
+ # Diff Diff Partial auto-correlation function for original time series
+ ax_pacf = fig.add_subplot(spec[1, 1])
+ sm.graphics.tsa.plot_pacf(
+ np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_pacf
+ )
+ plt.title(
+ f"Diff(Diff({ticker})) Partial Auto-Correlation from {start.strftime('%Y-%m-%d')}"
+ )
+
+ if gtff.USE_ION:
+ plt.ion()
+
+ plt.show()
+ print("")
+
+ except Exception as e:
+ print(e, "\n")
+ return
diff --git a/gamestonk_terminal/exploratory_data_analysis/eda_controller.py b/gamestonk_terminal/exploratory_data_analysis/eda_controller.py
new file mode 100644
index 00000000000..7d3189d1fe6
--- /dev/null
+++ b/gamestonk_terminal/exploratory_data_analysis/eda_controller.py
@@ -0,0 +1,177 @@
+"""Exploratory Data Analysis Controller Module"""
+__docformat__ = "numpy"
+
+import argparse
+from typing import List
+from datetime import datetime
+import pandas as pd
+from matplotlib import pyplot as plt
+from gamestonk_terminal.exploratory_data_analysis import eda_api
+from gamestonk_terminal import feature_flags as gtff
+from gamestonk_terminal.helper_funcs import get_flair
+from gamestonk_terminal.menu import session
+from prompt_toolkit.completion import NestedCompleter
+
+
+class EdaController:
+ """EDA Controller class"""
+
+ # Command choices
+ CHOICES = [
+ "help",
+ "q",
+ "quit",
+ "summary",
+ "hist",
+ "cdf",
+ "bwy",
+ "bwm",
+ "rolling",
+ "decompose",
+ "cusum",
+ "acf",
+ ]
+
+ def __init__(
+ self,
+ stock: pd.DataFrame,
+ ticker: str,
+ start: datetime,
+ interval: str,
+ ):
+ """Constructor"""
+ self.stock = stock
+ self.ticker = ticker
+ self.start = start
+ self.interval = interval
+ self.stats_parser = argparse.ArgumentParser(add_help=False, prog="stats")
+ self.stats_parser.add_argument(
+ "cmd",
+ choices=self.CHOICES,
+ )
+
+ @staticmethod
+ def print_help(self):
+ """Print help"""
+
+ s_intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"]
+
+ if self.start:
+ print(
+ f"\n{s_intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})"
+ )
+ else:
+ print(f"\n{s_intraday} Stock: {self.ticker}")
+
+ print("\nExploratory Data Analysis:")
+ print(" help show this comparison analysis menu again")
+ print(" q quit this menu, and shows back to main menu")
+ print(" quit quit to abandon program")
+ print("")
+ print(" summary brief summary statistics")
+ print(" hist histogram with density plot")
+ print(" cdf cumulative distribution function")
+ print(" bwy box and whisker yearly plot")
+ print(" bwm box and whisker monthly plot")
+ print(" rolling rolling mean and std deviation")
+ print(" decompose decomposition in cyclic-trend, season, and residuals")
+ print(" cusum detects abrupt changes using cumulative sum algorithm")
+ print(" acf (partial) auto-correlation function differentials")
+ print("")
+ return
+
+ def switch(self, an_input: str):
+ """Process and dispatch input
+
+ Returns
+ -------
+ True, False or None
+ False - quit the menu
+ True - quit the program
+ None - continue in the menu
+ """
+ (known_args, other_args) = self.stats_parser.parse_known_args(an_input.split())
+
+ return getattr(
+ self, "call_" + known_args.cmd, lambda: "Command not recognized!"
+ )(other_args)
+
+ def call_help(self, _):
+ """Process Help command"""
+ self.print_help(self)
+
+ def call_q(self, _):
+ """Process Q command - quit the menu"""
+ return False
+
+ def call_quit(self, _):
+ """Process Quit command - quit the program"""
+ return True
+
+ def call_summary(self, other_args: List[str]):
+ """Process summary command"""
+ eda_api.summary(other_args, self.stock)
+
+ def call_hist(self, other_args: List[str]):
+ """Process hist command"""
+ eda_api.hist(other_args, self.ticker, self.stock, self.start)
+
+ def call_cdf(self, other_args: List[str]):
+ """Process cdf command"""
+ eda_api.cdf(other_args, self.ticker, self.stock, self.start)
+
+ def call_bwy(self, other_args: List[str]):
+ """Process bwy command"""
+ eda_api.bwy(other_args, self.ticker, self.stock, self.start)
+
+ def call_bwm(self, other_args: List[str]):
+ """Process bwm command"""
+ eda_api.bwm(other_args, self.ticker, self.stock, self.start)
+
+ def call_rolling(self, other_args: List[str]):
+ """Process rolling command"""
+ eda_api.rolling(other_args, self.ticker, self.stock)
+
+ def call_decompose(self, other_args: List[str]):
+ """Process decompose command"""
+ eda_api.decompose(other_args, self.ticker, self.stock)
+
+ def call_cusum(self, other_args: List[str]):
+ """Process cusum command"""
+ eda_api.cusum(other_args, self.ticker, self.stock)
+
+ def call_acf(self, other_args: List[str]):
+ """Process acf command"""
+ eda_api.acf(other_args, self.ticker, self.stock, self.start)
+
+
+def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str):
+ """Statistics Menu"""
+
+ eda_controller = EdaController(stock, ticker, start, interval)
+ eda_controller.call_help(None)
+
+ while True:
+ # Get input command from user
+ if session and gtff.USE_PROMPT_TOOLKIT:
+ completer = NestedCompleter.from_nested_dict(
+ {c: None for c in eda_controller.CHOICES}
+ )
+ an_input = session.prompt(
+ f"{get_flair()} (eda)> ",
+ completer=completer,
+ )
+ else:
+ an_input = input(f"{get_flair()} (eda)> ")
+
+ try:
+ plt.close("all")
+
+ process_input = eda_controller.switch(an_input)
+
+ if process_input is not None:
+ return process_input
+
+ except SystemExit:
+ print("The command selected doesn't exist\n")
+ continue
diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py
index c5d95f067f3..cabd406e89c 100644
--- a/gamestonk_terminal/main_helper.py
+++ b/gamestonk_terminal/main_helper.py
@@ -75,6 +75,9 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open):
print(
" dd in-depth due-diligence, \t e.g.: news, analyst, shorts, insider, sec"
)
+ print(
+ " eda exploratory data analysis,\t e.g.: decompose, cusum, residuals analysis"
+ )
print(
" pred prediction techniques, \t e.g.: regression, arima, rnn, lstm, prophet"
)
diff --git a/poetry.lock b/poetry.lock
index cc79aeb62c3..98642b77bdf 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -436,6 +436,14 @@ wrapt = ">=1.10,<2"
[package.extras]
dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"]
+[[package]]
+name = "detecta"
+version = "0.0.5"
+description = "Detect events in data"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
[[package]]
name = "entrypoints"
version = "0.3"
@@ -2778,7 +2786,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t
[metadata]
lock-version = "1.1"
python-versions = "^3.6.8"
-content-hash = "b05035fef8aa3a5dacf6c4713579493f1104296ea25305049466fc47e0c35401"
+content-hash = "d0abec748e4bb0fcc0bb22409e4bfdcb6802f2ca9b1849a77941d5a4a954ccc2"
[metadata.files]
absl-py = [
@@ -3059,6 +3067,10 @@ deprecated = [
{file = "Deprecated-1.2.12-py2.py3-none-any.whl", hash = "sha256:08452d69b6b5bc66e8330adde0a4f8642e969b9e1702904d137eeb29c8ffc771"},
{file = "Deprecated-1.2.12.tar.gz", hash = "sha256:6d2de2de7931a968874481ef30208fd4e08da39177d61d3d4ebdf4366e7dbca1"},
]
+detecta = [
+ {file = "detecta-0.0.5-py3-none-any.whl", hash = "sha256:dbaf1938e5d785386c904034e2553824e2aa31793e967984ca65983aca06d23e"},
+ {file = "detecta-0.0.5.tar.gz", hash = "sha256:d2ea7d13dfbbc994d6ce385a7f8dc0a85fe675a8a8e712a64ec56e54c40603ed"},
+]
entrypoints = [
{file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
{file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
@@ -4288,6 +4300,7 @@ scikit-learn = [
{file = "scikit_learn-0.24.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c658432d8a20e95398f6bb95ff9731ce9dfa343fdf21eea7ec6a7edfacd4b4d9"},
{file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9dfa564ef27e8e674aa1cc74378416d580ac4ede1136c13dd555a87996e13422"},
{file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9c6097b6a9b2bafc5e0f31f659e6ab5e131383209c30c9e978c5b8abdac5ed2a"},
+ {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5580eba7345a4d3b097be2f067cc71a306c44bab19e8717a30361f279c929bea"},
{file = "scikit_learn-0.24.1-cp36-cp36m-win32.whl", hash = "sha256:7b04691eb2f41d2c68dbda8d1bd3cb4ef421bdc43aaa56aeb6c762224552dfb6"},
{file = "scikit_learn-0.24.1-cp36-cp36m-win_amd64.whl", hash = "sha256:1adf483e91007a87171d7ce58c34b058eb5dab01b5fee6052f15841778a8ecd8"},
{file = "scikit_learn-0.24.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:ddb52d088889f5596bc4d1de981f2eca106b58243b6679e4782f3ba5096fd645"},
@@ -4295,6 +4308,7 @@ scikit-learn = [
{file = "scikit_learn-0.24.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0567a2d29ad08af98653300c623bd8477b448fe66ced7198bef4ed195925f082"},
{file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:99349d77f54e11f962d608d94dfda08f0c9e5720d97132233ebdf35be2858b2d"},
{file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:83b21ff053b1ff1c018a2d24db6dd3ea339b1acfbaa4d9c881731f43748d8b3b"},
+ {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:abe835a851610f87201819cb315f8d554e1a3e8128912783a31e87264ba5ffb7"},
{file = "scikit_learn-0.24.1-cp37-cp37m-win32.whl", hash = "sha256:c3deb3b19dd9806acf00cf0d400e84562c227723013c33abefbbc3cf906596e9"},
{file = "scikit_learn-0.24.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d54dbaadeb1425b7d6a66bf44bee2bb2b899fe3e8850b8e94cfb9c904dcb46d0"},
{file = "scikit_learn-0.24.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:3c4f07f47c04e81b134424d53c3f5e16dfd7f494e44fd7584ba9ce9de2c5e6c1"},
@@ -4302,6 +4316,7 @@ scikit-learn = [
{file = "scikit_learn-0.24.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4ddd2b6f7449a5d539ff754fa92d75da22de261fd8fdcfb3596799fadf255101"},
{file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:826b92bf45b8ad80444814e5f4ac032156dd481e48d7da33d611f8fe96d5f08b"},
{file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:259ec35201e82e2db1ae2496f229e63f46d7f1695ae68eef9350b00dc74ba52f"},
+ {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:9599a3f3bf33f73fed0fe06d1dfa4e6081365a58c1c807acb07271be0dce9733"},
{file = "scikit_learn-0.24.1-cp38-cp38-win32.whl", hash = "sha256:8772b99d683be8f67fcc04789032f1b949022a0e6880ee7b75a7ec97dbbb5d0b"},
{file = "scikit_learn-0.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:ed9d65594948678827f4ff0e7ae23344e2f2b4cabbca057ccaed3118fdc392ca"},
{file = "scikit_learn-0.24.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8aa1b3ac46b80eaa552b637eeadbbce3be5931e4b5002b964698e33a1b589e1e"},
@@ -4309,6 +4324,7 @@ scikit-learn = [
{file = "scikit_learn-0.24.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:087dfede39efb06ab30618f9ab55a0397f29c38d63cd0ab88d12b500b7d65fd7"},
{file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:895dbf2030aa7337649e36a83a007df3c9811396b4e2fa672a851160f36ce90c"},
{file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9a24d1ccec2a34d4cd3f2a1f86409f3f5954cc23d4d2270ba0d03cf018aa4780"},
+ {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:54be0a60a5a35005ad69c75902e0f5c9f699db4547ead427e97ef881c3242e6f"},
{file = "scikit_learn-0.24.1-cp39-cp39-win32.whl", hash = "sha256:fab31f48282ebf54dd69f6663cd2d9800096bad1bb67bbc9c9ac84eb77b41972"},
{file = "scikit_learn-0.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:4562dcf4793e61c5d0f89836d07bc37521c3a1889da8f651e2c326463c4bd697"},
]
diff --git a/pyproject.toml b/pyproject.toml
index f4c3f068266..fb6675cf9a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ termcolor = "^1.1.0"
alpaca-trade-api = "^1.0.1"
pyally = "^1.1.2"
pycoingecko = "^1.4.0"
+detecta = "^0.0.5"
[tool.poetry.dev-dependencies]
pytest = "^6.2.2"
diff --git a/requirements-full.txt b/requirements-full.txt
index 24784254cfe..2ca49ab52d3 100644
--- a/requirements-full.txt
+++ b/requirements-full.txt
@@ -32,6 +32,7 @@ dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_
decorator==4.4.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6")
defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+detecta==0.0.5
entrypoints==0.3; python_version >= "3.6"
ephem==3.7.7.1; python_version >= "2.7" and python_version < "4"
fbprophet==0.6
diff --git a/requirements-old.txt b/requirements-old.txt
index 6aaede2a75a..21a289ed40a 100644
--- a/requirements-old.txt
+++ b/requirements-old.txt
@@ -23,6 +23,7 @@ cython==0.29.17; python_version >= "3.6" and python_full_version < "3.0.0" or py
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.6.2"
decorator==4.4.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6"
deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+detecta==0.0.5
ephem==3.7.7.1; python_version >= "2.7" and python_version < "4"
fbprophet==0.6
filelock==3.0.12; python_full_version >= "3.6.0" and python_version >= "3.6"
diff --git a/requirements-win.txt b/requirements-win.txt
index 7cebb528db2..0d6c1686522 100644
--- a/requirements-win.txt
+++ b/requirements-win.txt
@@ -23,6 +23,7 @@ cython==0.29.17; python_version >= "3.6" and python_full_version < "3.0.0" or py
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.6.2"
decorator==4.4.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6"
deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+detecta==0.0.5
ephem==3.7.7.1; python_version >= "2.7" and python_version < "4"
fbprophet==0.7.1
filelock==3.0.12; python_full_version >= "3.6.0" and python_version >= "3.6"
diff --git a/requirements.txt b/requirements.txt
index 919b6242f57..67f988879b7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,6 +24,7 @@ cycler==0.10.0; python_version >= "3.6"
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7"
decorator==4.4.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6")
defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+detecta==0.0.5
entrypoints==0.3; python_version >= "3.6"
finviz==1.3.4
fredapi==0.4.3
diff --git a/terminal.py b/terminal.py
index e9e9665eabc..f4ae5fde0b2 100644
--- a/terminal.py
+++ b/terminal.py
@@ -6,6 +6,7 @@ import sys
import os
from datetime import datetime, timedelta
import pandas as pd
+import yfinance as yf
from alpha_vantage.timeseries import TimeSeries
from prompt_toolkit.completion import NestedCompleter
@@ -23,6 +24,7 @@ from gamestonk_terminal.papermill import papermill_controller as mill
from gamestonk_terminal.behavioural_analysis import ba_controller
from gamestonk_terminal.technical_analysis import ta_menu as tam
from gamestonk_terminal.comparison_analysis import ca_controller
+from gamestonk_terminal.exploratory_data_analysis import eda_controller
from gamestonk_terminal.options import op_controller
from gamestonk_terminal.fred import fred_menu as fm
from gamestonk_terminal.portfolio import port_controller
@@ -75,6 +77,7 @@ def main():
"fa",
"ta",
"dd",
+ "eda",
"pred",
"ca",
"op",
@@ -202,6 +205,26 @@ def main():
elif ns_known_args.opt == "dd":
b_quit = ddm.dd_menu(df_stock, s_ticker, s_start, s_interval)
+ elif ns_known_args.opt == "eda":
+ if s_interval == "1440min":
+ b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval)
+ else:
+ df_stock = yf.download(s_ticker, start=s_start, progress=False)
+ df_stock = df_stock.rename(
+ columns={
+ "Open": "1. open",
+ "High": "2. high",
+ "Low": "3. low",
+ "Close": "4. close",
+ "Adj Close": "5. adjusted close",
+ "Volume": "6. volume",
+ }
+ )
+ df_stock.index.name = "date"
+ s_interval = "1440min"
+
+ b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval)
+
elif ns_known_args.opt == "op":
b_quit = op_controller.menu(s_ticker)