Esempio n. 1
0
 def main(self):
     """
 Pulls stock data for the ticker symbols from a json file and pull data from quandl, preprocesses the data
 and then build different supervised learning machine learning models and predicts future stock price.
 :return: None
 """
     logger.info(
         "------------------Started Stock Price Prediction-----------------"
     )
     # Create instances of all the classes used for stock prediction
     get_data = GetData(api_key=sys.argv[1])
     # Number of dates/data points into the future for which the stock price is to be predicted as a percentage of the
     # number of dates/data points for which historical data which is already available
     future_prediction_pcnt = 1
     preprocess_data = PreprocessData(
         future_prediction_pcnt=future_prediction_pcnt)
     build_models = BuildModels()
     forecast_prices = Predictions()
     # Get data from quandl.
     df = get_data.get_stock_data(update_data=False)
     # Preprocess data
     preprocessed_data_dict, original_df_dict = preprocess_data.preprocess_data(
         df, get_data.stock_ticker_list)
     models_list = [
         "Linear Regression", "Decision Tree Regressor",
         "Random Forest Regressor"
     ]
     # Build models
     models_dict, model_scores_dict = build_models.build_models(
         models_list, preprocessed_data_dict, force_build=False)
     # Predict future stock prices
     forecast_df_dict = forecast_prices.make_predictions(
         models_dict, preprocessed_data_dict, original_df_dict)
     self.plot_forecast(forecast_df_dict, original_df_dict,
                        future_prediction_pcnt)
Esempio n. 2
0
    def __init__(self):
        self.window = Tk()
        self.window.protocol("WM_DELETE_WINDOW", self.finishApp)
        self.window.title('Herramienta de prueba para la predicción')
        ico = PhotoImage(file=os.path.abspath('.') + '/Activos/term.png')
        self.window.call('wm', 'iconphoto', self.window._w, ico)
        self.window.config(bg=mainColor)
        self.window.attributes("-zoomed", False)
        self.window.minsize(width=300, height=300)

        #Peso 100% en self.window
        self.window.grid_rowconfigure(0, weight=1)
        self.window.grid_columnconfigure(0, weight=1)

        #Ventana principal fija sin ajuste de tamaño
        self.window.resizable(False, False)

        #Centrar ventana en función de las dimendsiones del contenedor de inicio
        self.center(450, 490)

        self.view = Window(self.window)
        self.functions = Functions()

        self.infoW = Info(self.view.dataContainer)
        self.genDataW = GenData(self.view.dataContainer)
        self.predW = Predictions(self.view.dataContainer)
        self.graphsW = Graphs(self.view.dataContainer)

        #Botones de las acciones del usuario
        self.view.buttonDisconnect.bind("<Button>", self.disconnect)
        self.view.buttonToBD.bind("<Button>", self.changeToConnect)
        self.view.buttonConnect.bind("<Button>", self.checkConnection)
        self.view.infoButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.predButton, self.view.graphButton, self.view.
                genDataButton, self.view.infoButton, 1))
        self.view.genDataButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.predButton, self.view.
                graphButton, self.view.genDataButton, 2))
        self.view.predButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.graphButton, self.view.
                genDataButton, self.view.predButton, 3))
        self.view.graphButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.genDataButton, self.view.
                predButton, self.view.graphButton, 4))
        self.genDataW.buttonFilterData.bind("<Button>",
                                            lambda event: self.getNewGenData())
        self.graphsW.buttonGetGraph.bind("<Button>",
                                         lambda event: self.getGraphsData())
        self.predW.buttonForecast.bind("<Button>", self.getForecast)
        self.predW.butHistForecast.bind("<Button>", self.getHistForecast)

        #Ventana principal actualizaciones
        self.window.mainloop()
 def __init__(self, wotv_bot_config: WotvBotConfig):
     self.wotv_bot_config = wotv_bot_config
     # Set this to true in an integration test to allow a local filesystem path to be used in a Discord
     # message as the source of the image to be processed by OCR for Vision Card text extraction. For
     # obvious security reasons, this is false by default.
     self.INTEG_TEST_LOCAL_FILESYSTEM_READ_FOR_VISION_CARD = False
     # Set the static instance of the bot to this instance.
     WotvBot.__staticInstance = self
     self.whimsy_shop_nrg_reminder_delay_ms: int = 30 * 60 * 1000  # 30 minutes
     self.whimsy_shop_spawn_reminder_delay_ms: int = 60 * 60 * 1000  # 60 minutes
     self.predictions = Predictions('predictions.txt')
     self.predictions.refreshPredictions()
     self.last_status = None  # Last status set
Esempio n. 4
0
    def makeAllPredictions(self,
                           algorithms,
                           outputFolder,
                           topPredictionsFolder=None):

        self.updateGraphByOneNavigation()

        if self.navPath.getLength() < 2:
            raise RuntimeError(
                'makeAllPredictions: Not enough navigations to run predictive algorithms'
            )

        # Build the output data structure
        results = {}
        for algorithm in algorithms:
            results[algorithm.name] = Predictions(algorithm.name, outputFolder,
                                                  algorithm.fileName,
                                                  algorithm.includeTop,
                                                  topPredictionsFolder)

        totalPredictions = self.navPath.getLength() - 1

        for _ in range(1, totalPredictions + 1):
            self.updateGraphByOneNavigation()
            print 'Making predictions for navigation #' + str(
                self.navNumber) + ' of ' + str(totalPredictions)
            for algorithm in algorithms:
                results[algorithm.name].addPrediction(
                    self.__makePrediction(algorithm))

        print 'Done making predictions.'
        print self.graph.printEntireGraphStats()
        return results
Esempio n. 5
0
    def get_model_predictions(self, model, model_name, data):
        tl_file_name = self.get_tl_file_name(model_name)
        if os.path.isfile(tl_file_name):
            model.load_weights(tl_file_name)
        probs = model.predict([data, data, data])
        preds = probs.argmax(axis=-1)

        return Predictions(model_name, preds)
Esempio n. 6
0
    def getPredictions(self):

        url = "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a={}".format(self.agency.tag)

        for stop in self.stopList:
            url += "&stops={}|{}".format(self.tag,stop.tag)

        response = requests.get(url)

        body = ElementTree.fromstring(response.content)

        predictions = []
        for predictionsTag in body.findall('predictions'):
            predictions.append(Predictions(predictionsTag).toDict())
        return predictions
Esempio n. 7
0
def handlePredictions(previous_rating, new_rating, channel_name, client_id,
                      access_token, twitch_id):
    print("previous_rating: " + str(previous_rating))
    print("new_rating: " + str(new_rating))

    predicter = Predictions(channel_name, twitch_id, client_id, access_token)
    # Rating gain
    if new_rating > previous_rating:
        predicter.run(True)
    # Rating loss
    elif new_rating < previous_rating:
        predicter.run(False)
    # No Rating Change
    else:
        pass  # no way to determine if a 0 mmr game occured
Esempio n. 8
0
class Aplicacion():

    #Inicialización
    def __init__(self):
        self.window = Tk()
        self.window.protocol("WM_DELETE_WINDOW", self.finishApp)
        self.window.title('Herramienta de prueba para la predicción')
        ico = PhotoImage(file=os.path.abspath('.') + '/Activos/term.png')
        self.window.call('wm', 'iconphoto', self.window._w, ico)
        self.window.config(bg=mainColor)
        self.window.attributes("-zoomed", False)
        self.window.minsize(width=300, height=300)

        #Peso 100% en self.window
        self.window.grid_rowconfigure(0, weight=1)
        self.window.grid_columnconfigure(0, weight=1)

        #Ventana principal fija sin ajuste de tamaño
        self.window.resizable(False, False)

        #Centrar ventana en función de las dimendsiones del contenedor de inicio
        self.center(450, 490)

        self.view = Window(self.window)
        self.functions = Functions()

        self.infoW = Info(self.view.dataContainer)
        self.genDataW = GenData(self.view.dataContainer)
        self.predW = Predictions(self.view.dataContainer)
        self.graphsW = Graphs(self.view.dataContainer)

        #Botones de las acciones del usuario
        self.view.buttonDisconnect.bind("<Button>", self.disconnect)
        self.view.buttonToBD.bind("<Button>", self.changeToConnect)
        self.view.buttonConnect.bind("<Button>", self.checkConnection)
        self.view.infoButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.predButton, self.view.graphButton, self.view.
                genDataButton, self.view.infoButton, 1))
        self.view.genDataButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.predButton, self.view.
                graphButton, self.view.genDataButton, 2))
        self.view.predButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.graphButton, self.view.
                genDataButton, self.view.predButton, 3))
        self.view.graphButton.bind(
            "<Button>", lambda event: self.switchSelector(
                self.view.infoButton, self.view.genDataButton, self.view.
                predButton, self.view.graphButton, 4))
        self.genDataW.buttonFilterData.bind("<Button>",
                                            lambda event: self.getNewGenData())
        self.graphsW.buttonGetGraph.bind("<Button>",
                                         lambda event: self.getGraphsData())
        self.predW.buttonForecast.bind("<Button>", self.getForecast)
        self.predW.butHistForecast.bind("<Button>", self.getHistForecast)

        #Ventana principal actualizaciones
        self.window.mainloop()

    #Centra la ventana
    def center(self, width, height):
        w = self.window.winfo_screenwidth()
        h = self.window.winfo_screenheight()
        x = int((w - width) / 2)
        y = int((h - height) / 2)
        self.window.geometry('+{}+{}'.format(x, y))

    #Función para comprobar a conexión con la base de datos con los parametros introducidos
    def checkConnection(self, event):
        global connectionData
        connectionData['hostDir'] = self.view.dir.get()
        connectionData['serverUser'] = self.view.user.get()
        connectionData['userPassword'] = self.view.password.get()
        connectionData['port'] = self.view.portEntry.get()
        connectionData['databaseName'] = self.view.dataB.get()
        if (connectionData['hostDir'] == None
                or connectionData['hostDir'] == ''):
            resultConnection = 'Debe indicar la dirección del servidor'
        elif (connectionData['serverUser'] == None
              or connectionData['serverUser'] == ''):
            resultConnection = 'Debe introducir un usuario'
        elif (connectionData['userPassword'] == None):
            resultConnection = 'Debe introducir una contraseña'
        elif (connectionData['port'] == None or connectionData['port'] == ''):
            resultConnection = 'Debe indicar el puerto de conexión'
        elif (connectionData['databaseName'] == None
              or connectionData['databaseName'] == ''):
            resultConnection = 'Debe indicar la base de datos a usar'
        else:
            global bd
            bd = BD(connectionData)
            resultConnection = bd.connect()
        self.view.setConnectionResult(self.window, resultConnection)
        if (resultConnection == True):
            #Se obtienen los nombres de las tablas a mostrar
            estData = bd.getTableNames(connectionData['databaseName'])
            #Se olvida el frame de conexion
            self.view.firstContainer.grid_forget()
            #Ventana principal fija sin ajuste de tamaño
            self.window.resizable(True, True)
            #Se establece el frame princial y sus widgets
            self.view.mainView(self.window)
            self.genDataW.complete(estData)
            self.predW.complete(estData)
            self.graphsW.complete(estData)
            self.getNewGenData()
            self.getGraphsData()
            self.genDataW.hideGenData()
            self.predW.hidePredictions()
            self.graphsW.hideGraphs()
            self.infoW.showInfo()

    #Pasar a la vista de conexxion
    def changeToConnect(self, event):
        #Olvidar el grid de start container y eliminarlo
        self.view.startContainer.grid_forget()
        self.view.startContainer.destroy()
        self.view.firstContainer.grid(row=0, column=0)

    #Función para elegir pestaña en la ventana principal
    def switchSelector(self, button1, button2, button3, button4, number):
        if (number == 1):
            self.genDataW.hideGenData()
            self.predW.hidePredictions()
            self.graphsW.hideGraphs()
            self.infoW.showInfo()
        elif (number == 2):
            self.infoW.hideInfo()
            self.predW.hidePredictions()
            self.graphsW.hideGraphs()
            self.genDataW.showGenData()
        elif (number == 3):
            self.infoW.hideInfo()
            self.genDataW.hideGenData()
            self.graphsW.hideGraphs()
            self.predW.showPredictions()
        else:
            self.infoW.hideInfo()
            self.genDataW.hideGenData()
            self.predW.hidePredictions()
            self.graphsW.showGraphs()

        button1.config(bg=mainColor, activebackground=thirdColor)
        button2.config(bg=mainColor, activebackground=thirdColor)
        button3.config(bg=mainColor, activebackground=thirdColor)
        button4.config(bg=secondaryColor, activebackground=secondaryColor)

    #Filtra los datos que se tienen que mostrar en la pestaña de datos generales
    def getNewGenData(self):
        self.genDataW.textResult.set('')
        if (self.genDataW.desdeE.get() != ''
                and self.genDataW.hastaE.get() != ''):
            try:
                d = datetime(int(self.genDataW.desdeE.get()[0:4]),
                             int(self.genDataW.desdeE.get()[5:7]),
                             int(self.genDataW.desdeE.get()[8:10]))
                h = datetime(int(self.genDataW.hastaE.get()[0:4]),
                             int(self.genDataW.hastaE.get()[5:7]),
                             int(self.genDataW.hastaE.get()[8:10]), 23, 00, 00)
                if (d <= h):
                    result = 1
                    result = self.functions.getGenData(bd, d, h)
                else:
                    self.genDataW.textResult.set(
                        'La primera fecha debe ser menor que la segunda')
                    return
            except:
                self.genDataW.textResult.set(
                    'El formato de las fechas es incorrecto')
                return
        elif (self.genDataW.desdeE.get() == ''
              and self.genDataW.hastaE.get() != ''):
            try:
                h = datetime(int(self.genDataW.hastaE.get()[0:4]),
                             int(self.genDataW.hastaE.get()[5:7]),
                             int(self.genDataW.hastaE.get()[8:10]), 23, 00, 00)
                result = self.functions.getGenData(bd,
                                                   self.genDataW.desdeE.get(),
                                                   h)
            except:
                self.genDataW.textResult.set(
                    'El formato de las fechas es incorrecto')
                return
        elif (self.genDataW.desdeE.get() != ''
              and self.genDataW.hastaE.get() == ''):
            try:
                d = datetime(int(self.genDataW.desdeE.get()[0:4]),
                             int(self.genDataW.desdeE.get()[5:7]),
                             int(self.genDataW.desdeE.get()[8:10]))
                result = self.functions.getGenData(bd, d,
                                                   self.genDataW.hastaE.get())
            except:
                self.genDataW.textResult.set(
                    'El formato de las fechas es incorrecto')
                return
        else:
            result = self.functions.getGenData(bd, self.genDataW.desdeE.get(),
                                               self.genDataW.hastaE.get())
        if (result == None):
            self.genDataW.textResult.set(
                'No hay datos para las fechas seleccionadas')
        else:
            self.genDataW.start.set(result[1])
            self.genDataW.finish.set(result[2])
            self.genDataW.changeTableValues(result[0])

    #Filtra los datos de los gráficos
    def getGraphsData(self):
        self.graphsW.textGraphResult.set('')
        estacion = self.graphsW.localization.get()
        meteoData = self.graphsW.atr.get()
        if (estacion == ''):
            self.graphsW.textGraphResult.set('Debe elegir una localización')
        elif (meteoData == ''):
            self.graphsW.textGraphResult.set('Debe elegir una variable')
        else:
            if (self.graphsW.dgEntry.get() != ''
                    and self.graphsW.hgEntry.get() != ''):
                try:
                    d = datetime(int(self.graphsW.dgEntry.get()[0:4]),
                                 int(self.graphsW.dgEntry.get()[5:7]),
                                 int(self.graphsW.dgEntry.get()[8:10]))
                    h = datetime(int(self.graphsW.hgEntry.get()[0:4]),
                                 int(self.graphsW.hgEntry.get()[5:7]),
                                 int(self.graphsW.hgEntry.get()[8:10]), 23, 00,
                                 00)
                    if (d <= h):
                        result = 1
                        result = bd.selectFromTable(estacion, d, h)
                    else:
                        self.graphsW.textGraphResult.set(
                            'La primera fecha debe ser menor que la segunda')
                        return
                except:
                    self.graphsW.textGraphResult.set(
                        'El formato de las fechas es incorrecto')
                    return
            elif (self.graphsW.dgEntry.get() == ''
                  and self.graphsW.hgEntry.get() != ''):
                try:
                    h = datetime(int(self.graphsW.hgEntry.get()[0:4]),
                                 int(self.graphsW.hgEntry.get()[5:7]),
                                 int(self.graphsW.hgEntry.get()[8:10]), 23, 00,
                                 00)
                    result = bd.selectFromTable(estacion,
                                                self.graphsW.dgEntry.get(), h)
                except:
                    self.graphsW.textGraphResult.set(
                        'El formato de las fechas es incorrecto')
                    return
            elif (self.graphsW.dgEntry.get() != ''
                  and self.graphsW.hgEntry.get() == ''):
                try:
                    d = datetime(int(self.graphsW.dgEntry.get()[0:4]),
                                 int(self.graphsW.dgEntry.get()[5:7]),
                                 int(self.graphsW.dgEntry.get()[8:10]))
                    result = bd.selectFromTable(estacion, d,
                                                self.graphsW.hgEntry.get())
                except:
                    self.graphsW.textGraphResult.set(
                        'El formato de las fechas es incorrecto')
                    return
            else:
                result = bd.selectFromTable(estacion,
                                            self.graphsW.dgEntry.get(),
                                            self.graphsW.hgEntry.get())

            if (len(result) == 0):
                self.graphsW.textGraphResult.set(
                    'No hay datos para las fechas seleccionadas')

            else:
                figure = self.functions.graphData(
                    self.graphsW.localization.get(), self.graphsW.atr.get(),
                    result)
                self.graphsW.newGraph(figure)

    #Función al pulsar el botón de predecir
    def getForecast(self, event):
        self.predW.butHistForecast.grid_forget()
        self.predW.resultPredContainer.grid_columnconfigure(2, weight=0)
        self.predW.predErr.set('')
        self.predW.predResult.set('')
        self.predW.textForecast.set('')
        atr = []
        vars = []
        if (self.predW.loc.get() == 'None'):
            self.predW.predErr.set(
                'Debe seleccionar una localización para poder completar la predicción'
            )
            return
        elif (self.predW.t.get() == 0 and self.predW.w.get() == 0
              and self.predW.p.get() == 0 and self.predW.h.get() == 0
              and self.predW.d.get() == 0):
            self.predW.predErr.set(
                'Debe seleccionar al menos una variable para poder completar la predicción'
            )
            return
        elif (self.predW.mod.get() == 'None'):
            self.predW.predErr.set(
                'Debe seleccionar una modelo de predicción para poder completar la predicción'
            )
            return
        else:
            if (self.predW.t.get() == 1):
                atr.append('temperatura')
            if (self.predW.w.get() == 1):
                atr.append('viento')
            if (self.predW.d.get() == 1):
                atr.append('direccion')
            if (self.predW.p.get() == 1):
                atr.append('presion')
            if (self.predW.h.get() == 1):
                atr.append('humedad')

            if (self.predW.mod.get() == 'K-NN'):
                vars.append(self.predW.neighbors.get())
            elif (self.predW.mod.get() == 'Red neuronal'):
                vars.append(self.predW.layers.get())
                vars.append(self.predW.neurons1.get())
                vars.append(self.predW.neurons2.get())
                vars.append(self.predW.neurons3.get())
                vars.append(self.predW.itersRedNeu.get())
                vars.append(self.predW.learnRate.get())
            elif (self.predW.mod.get() == 'Árbol de decisión'):
                if (self.predW.maxDepth.get() != 0):
                    vars.append(self.predW.maxDepth.get())
                else:
                    vars.append(None)
            else:
                None

            res = bd.selectDataForecast(
                self.predW.loc.get(),
                self.predW.iter.get() + int(self.predW.dayRef.get()), False)
            result = self.functions.valoresTempMaximas(res)
            pred = self.functions.dataForecast(result, self.predW.mod.get(),
                                               atr, vars,
                                               int(self.predW.dayRef.get()),
                                               self.predW.iter.get())
            self.predW.showPrediction(pred)

    #Mostrar gráfico con historico de predicciones
    def getHistForecast(self, event):
        self.predW.predErr.set('')
        atr = []
        vars = []
        if (self.predW.loc.get() == 'None'):
            self.predW.predErr.set(
                'Debe seleccionar una localización para poder completar la predicción'
            )
            return
        elif (self.predW.t.get() == 0 and self.predW.w.get() == 0
              and self.predW.p.get() == 0 and self.predW.h.get() == 0
              and self.predW.d.get() == 0):
            self.predW.predErr.set(
                'Debe seleccionar al menos una variable para poder completar la predicción'
            )
            return
        elif (self.predW.mod.get() == 'None'):
            self.predW.predErr.set(
                'Debe seleccionar una modelo de predicción para poder completar la predicción'
            )
            return
        else:
            if (self.predW.t.get() == 1):
                atr.append('temperatura')
            if (self.predW.w.get() == 1):
                atr.append('viento')
            if (self.predW.d.get() == 1):
                atr.append('direccion')
            if (self.predW.p.get() == 1):
                atr.append('presion')
            if (self.predW.h.get() == 1):
                atr.append('humedad')

            if (self.predW.mod.get() == 'K-NN'):
                vars.append(self.predW.neighbors.get())
            elif (self.predW.mod.get() == 'Red neuronal'):
                vars.append(self.predW.layers.get())
                vars.append(self.predW.neurons1.get())
                vars.append(self.predW.neurons2.get())
                vars.append(self.predW.neurons3.get())
                vars.append(self.predW.itersRedNeu.get())
                vars.append(self.predW.learnRate.get())
            elif (self.predW.mod.get() == 'Árbol de decisión'):
                if (self.predW.maxDepth.get() != 0):
                    vars.append(self.predW.maxDepth.get())
                else:
                    vars.append(None)
            else:
                None

            res = bd.selectDataForecast(
                self.predW.loc.get(),
                self.predW.iter.get() + int(self.predW.dayRef.get()), True)
            result = self.functions.valoresTempMaximas(res)
            figure = self.functions.AllForecast(result, self.predW.mod.get(),
                                                atr, vars,
                                                int(self.predW.dayRef.get()),
                                                self.predW.iter.get())
            self.predW.showHist(figure)

    #Función para descconectar de la base de datos
    def disconnect(self, event):
        global bd
        self.view.execute = False
        try:
            bd.close()
        except:
            None
        #Olvidar self.mainContainer
        self.view.mainContainer.grid_forget()
        #Atributos de la ventana
        self.window.attributes("-zoomed", False)
        self.window.geometry('450x490')
        self.center(450, 490)
        self.window.resizable(False, False)
        #Cargar connection container
        self.view.firstContainer.grid(row=0, column=0)
        self.view.connectionView(self.window)

    def finishApp(self):
        global bd
        self.view.execute = False
        try:
            bd.close()
        except:
            None
        try:
            self.view.t1.join(0)
            self.view.t2.join(1)
        except:
            None

        sys.exit(0)
class WotvBot:
    """An instance of the bot, configured to manage specific spreadsheets and using Discord and Google credentials."""

    # The static instance of the bot, not for general consumption.
    __staticInstance: WotvBot = None

    def __init__(self, wotv_bot_config: WotvBotConfig):
        self.wotv_bot_config = wotv_bot_config
        # Set this to true in an integration test to allow a local filesystem path to be used in a Discord
        # message as the source of the image to be processed by OCR for Vision Card text extraction. For
        # obvious security reasons, this is false by default.
        self.INTEG_TEST_LOCAL_FILESYSTEM_READ_FOR_VISION_CARD = False
        # Set the static instance of the bot to this instance.
        WotvBot.__staticInstance = self
        self.whimsy_shop_nrg_reminder_delay_ms: int = 30 * 60 * 1000  # 30 minutes
        self.whimsy_shop_spawn_reminder_delay_ms: int = 60 * 60 * 1000  # 60 minutes
        self.predictions = Predictions('predictions.txt')
        self.predictions.refreshPredictions()
        self.last_status = None  # Last status set

    @staticmethod
    def getStaticInstance():
        """Returns an unsafe static reference to the "current" bot, if there is one. In reality this is just the most recently-created bot.

        Use with extreme caution. This is primarily intended for internal use cases where a static method is required, such as the callback
        for a "apscheduler"-module task such as a reminder that is being invoked asynchronously and potentially across different instances of
        the bot process where the specific instance of the bot is irrelevant.
        """
        return WotvBot.__staticInstance

    async def handleMessage(self, message: discord.Message):
        """Process the request and produce a response."""
        # Bail out early if anything looks insane.
        if message.author == self.wotv_bot_config.discord_client.user:
            return (None, None)
        if not message.content:
            return (None, None)
        if not message.content.startswith('!'):
            return (None, None)
        for ignore_pattern in WotvBotConstants.ALL_IGNORE_PATTERNS:
            if ignore_pattern.match(message.content):
                return (None, None)

        # Set up the context used in handling every possible command.
        # TODO: Clean up these fields that are not part of the CommandContextInfo object.
        from_name = message.author.display_name
        from_id = message.author.id
        from_discrim = message.author.discriminator
        context = CommandContextInfo()
        context.from_discrim = from_discrim
        context.from_id = from_id
        context.from_name = from_name
        context.original_message = message

        # TODO: Hold these references longer after cleaning up the rest of the code, in an application context.
        esper_resonance_manager = EsperResonanceManager(
            self.wotv_bot_config.esper_resonance_spreadsheet_id,
            self.wotv_bot_config.sandbox_esper_resonance_spreadsheet_id,
            self.wotv_bot_config.access_control_spreadsheet_id,
            self.wotv_bot_config.spreadsheet_app)
        vision_card_manager = VisionCardManager(
            self.wotv_bot_config.vision_card_spreadsheet_id,
            self.wotv_bot_config.access_control_spreadsheet_id,
            self.wotv_bot_config.spreadsheet_app)

        # To support multi-line commands, we only match the command itself against the first line.
        first_line_lower = message.content.splitlines()[0].lower()

        match = WotvBotConstants.RES_FETCH_SELF_PATTERN.match(first_line_lower)
        if match:
            return self.handleTargetedResonanceLookupForSelf(
                context.shallowCopy().withMatch(
                    match).withEsperResonanceManager(esper_resonance_manager))

        match = WotvBotConstants.RES_LIST_SELF_PATTERN.match(first_line_lower)
        if match:
            return self.handleGeneralResonanceLookupForSelf(
                context.shallowCopy().withMatch(
                    match).withEsperResonanceManager(esper_resonance_manager))

        match = WotvBotConstants.RES_FETCH_OTHER_PATTERN.match(
            first_line_lower)
        if match:
            return self.handleTargetedResonanceLookupForOtherUser(
                context.shallowCopy().withMatch(
                    match).withEsperResonanceManager(esper_resonance_manager))

        match = WotvBotConstants.RES_SET_PATTERN.match(first_line_lower)
        if match:
            return self.handleResonanceSet(context.shallowCopy().withMatch(
                match).withEsperResonanceManager(esper_resonance_manager))

        if WotvBotConstants.VISION_CARD_SET_PATTERN.match(first_line_lower):
            return await self.handleVisionCardSet(
                context.shallowCopy().withVisionCardManager(
                    vision_card_manager))

        match = WotvBotConstants.VISION_CARD_FETCH_BY_NAME_PATTERN.match(
            first_line_lower)
        if match:
            return await self.handleVisionCardFetchByName(
                context.shallowCopy().withMatch(match).withVisionCardManager(
                    vision_card_manager))

        match = WotvBotConstants.VISION_CARD_ABILITY_SEARCH.match(
            first_line_lower)
        if match:
            return await self.handleVisionCardAbilitySearch(
                context.shallowCopy().withMatch(match).withVisionCardManager(
                    vision_card_manager))

        if WotvBotConstants.VISION_CARD_DEBUG_PATTERN.match(first_line_lower):
            return await self.handleVisionCardDebug(
                context.shallowCopy().withVisionCardManager(
                    vision_card_manager))

        match = WotvBotConstants.FIND_SKILLS_BY_NAME_PATTERN.match(
            first_line_lower)
        if match:
            return await self.handleFindSkillsByName(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.FIND_SKILLS_BY_DESCRIPTION_PATTERN.match(
            first_line_lower)
        if match:
            return await self.handleFindSkillsByDescription(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.RICH_UNIT_SEARCH_PATTERN.match(
            first_line_lower)
        if match:
            return await self.handleRichUnitSearch(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.WHIMSY_REMINDER_PATTERN.match(
            first_line_lower)
        if match:
            return await self.handleWhimsyReminder(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.ROLLDICE_PATTERN.match(first_line_lower)
        if match:
            return await self.handleRoll(context.shallowCopy().withMatch(match)
                                         )

        # Predictions
        match = WotvBotConstants.PREDICTION_PATTERN_1.match(first_line_lower)
        if match:
            return await self.handlePrediction(
                context.shallowCopy().withMatch(match))
        match = WotvBotConstants.PREDICTION_PATTERN_2.match(first_line_lower)
        if match:
            return await self.handlePrediction(
                context.shallowCopy().withMatch(match))
        match = WotvBotConstants.PREDICTION_PATTERN_3.match(first_line_lower)
        if match:
            return await self.handlePrediction(
                context.shallowCopy().withMatch(match))
        match = WotvBotConstants.PREDICTION_PATTERN_4.match(first_line_lower)
        if match:
            return await self.handlePrediction(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.DOUBLE_DROP_RATES_SCHEDULE_PATTERN_1.match(
            first_line_lower)
        if match:
            return await self.handleSchedule(
                context.shallowCopy().withMatch(match))

        match = WotvBotConstants.DOUBLE_DROP_RATES_SCHEDULE_PATTERN_2.match(
            first_line_lower)
        if match:
            return await self.handleMats(context.shallowCopy().withMatch(match)
                                         )

        match = WotvBotConstants.DAILY_REMINDERS.match(first_line_lower)
        if match:
            return await self.handleDailyReminders(
                context.shallowCopy().withMatch(match))

        # Hidden utility command to look up the snowflake ID of your own user. This isn't secret or insecure, but it's also not common, so it isn't listed.
        if first_line_lower.startswith('!whoami'):
            return self.handleWhoAmI(context)

        # Hidden utility command to look up the snowflake ID of a member. This isn't secret or insecure, but it's also not common, so it isn't listed.
        match = WotvBotConstants.WHOIS_PATTERN.match(first_line_lower)
        if match:
            return await self.handleWhoIs(
                context.shallowCopy().withMatch(match))

        if WotvBotConstants.ADMIN_ADD_ESPER_PATTERN.match(
                first_line_lower
        ) or WotvBotConstants.SANDBOX_ADMIN_ADD_ESPER_PATTERN.match(
                message.content):
            return self.handleAdminAddEsper(
                context.shallowCopy().withEsperResonanceManager(
                    esper_resonance_manager))

        if WotvBotConstants.ADMIN_ADD_UNIT_PATTERN.match(
                first_line_lower
        ) or WotvBotConstants.SANDBOX_ADMIN_ADD_UNIT_PATTERN.match(
                message.content):
            return self.handleAdminAddUnit(
                context.shallowCopy().withEsperResonanceManager(
                    esper_resonance_manager))

        if WotvBotConstants.ADMIN_ADD_VC_PATTERN.match(first_line_lower):
            return self.handleAdminAddVisionCard(
                context.shallowCopy().withVisionCardManager(
                    vision_card_manager))

        if WotvBotConstants.ADMIN_ADD_USER_PATTERN.match(first_line_lower):
            return self.handleAdminAddUser(
                context.shallowCopy().withEsperResonanceManager(
                    esper_resonance_manager).withVisionCardManager(
                        vision_card_manager))

        if first_line_lower.startswith('!resonance'):
            responseText = '<@{0}>: Invalid !resonance command. Use !help for more information.'.format(
                from_id)
            return (responseText, None)

        if first_line_lower.startswith('!help'):
            responseText = WotvBotConstants.HELP.format(
                self.wotv_bot_config.esper_resonance_spreadsheet_id,
                self.wotv_bot_config.vision_card_spreadsheet_id)
            return (responseText, None)

        return ('<@{0}>: Invalid or unknown command. Use !help to see all supported commands and !admin-help to see special admin commands. '\
                'Please do this via a direct message to the bot, to avoid spamming the channel.'.format(from_id), None)

    def handleTargetedResonanceLookupForSelf(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !res command for self-lookup of a specific (unit, esper) tuple."""
        unit_name = context.command_match.group(1).strip()
        esper_name = context.command_match.group(2).strip()
        print(
            'resonance fetch from user %s#%s, for user %s, for unit %s, for esper %s'
            % (context.from_name, context.from_discrim, context.from_name,
               unit_name, esper_name))
        resonance, pretty_unit_name, pretty_esper_name = context.esper_resonance_manager.readResonance(
            None, context.from_id, unit_name, esper_name)
        responseText = '<@{0}>: {1}/{2} has resonance {3}'.format(
            context.from_id, pretty_unit_name, pretty_esper_name, resonance)
        return (responseText, None)

    def handleTargetedResonanceLookupForOtherUser(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !res command for lookup of a specific (unit, esper) tuple for a different user."""
        target_user_name = context.command_match.group(1).strip()
        unit_name = context.command_match.group(2).strip()
        esper_name = context.command_match.group(3).strip()
        print(
            'resonance fetch from user %s#%s, for user %s, for unit %s, for esper %s'
            % (context.from_name, context.from_discrim, target_user_name,
               unit_name, esper_name))
        resonance, pretty_unit_name, pretty_esper_name = context.esper_resonance_manager.readResonance(
            target_user_name, None, unit_name, esper_name)
        responseText = '<@{0}>: for user {1}, {2}/{3} has resonance {4}'.format(
            context.from_id, target_user_name, pretty_unit_name,
            pretty_esper_name, resonance)
        return (responseText, None)

    def handleGeneralResonanceLookupForSelf(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !res command for self-lookup of all resonance for a given unit or esper."""
        target_name = context.command_match.group('target_name').strip()
        print('resonance list fetch from user %s#%s, for target %s' %
              (context.from_name, context.from_discrim, target_name))
        pretty_name, resonance_listing = context.esper_resonance_manager.readResonanceList(
            None, context.from_id, target_name)
        responseText = '<@{0}>: resonance listing for {1}:\n{2}'.format(
            context.from_id, pretty_name, resonance_listing)
        return (responseText, None)

    def handleResonanceSet(self, context: CommandContextInfo) -> (str, str):
        """Handle !res-set command to set resonance for a specific unit and esper tuple."""
        unit_name = context.command_match.group('unit').strip()
        esper_name = context.command_match.group('esper').strip()
        resonance_numeric_string = context.command_match.group(
            'resonance_level').strip()
        priority = None
        if context.command_match.group('priority'):
            priority = context.command_match.group('priority').strip()
        comment = None
        if context.command_match.group('comment'):
            comment = context.command_match.group('comment').strip()
        print(
            'resonance set from user %s#%s, for unit %s, for esper %s, to resonance %s, with priority %s, comment %s'
            % (context.from_name, context.from_discrim, unit_name, esper_name,
               resonance_numeric_string, priority, comment))
        old_resonance, new_resonance, pretty_unit_name, pretty_esper_name = context.esper_resonance_manager.setResonance(
            context.from_id, unit_name, esper_name, resonance_numeric_string,
            priority, comment)
        responseText = '<@{0}>: {1}/{2} resonance has been set to {3} (was: {4})'.format(
            context.from_id, pretty_unit_name, pretty_esper_name,
            new_resonance, old_resonance)
        if (resonance_numeric_string and int(resonance_numeric_string) == 10):
            # reaction = '\U0001F4AA' # CLDR: flexed biceps
            reaction = '\U0001F3C6'  # CLDR: trophy
        else:
            reaction = '\U00002705'  # CLDR: check mark button
        return (responseText, reaction)

    def handleWhoAmI(self, context: CommandContextInfo) -> (str, str):
        """Handle !whoami command to fetch your own snowflake ID."""
        responseText = '<@{id}>: Your snowflake ID is {id}'.format(
            id=context.from_id)
        return (responseText, None)

    async def handleWhoIs(self, context: CommandContextInfo) -> (str, str):
        """Handle !whois command to fetch the snowflake ID for a given user."""
        original_match = WotvBotConstants.WHOIS_PATTERN.match(
            context.original_message.content)  # Fetch original-case name
        target_member_name = original_match.group('server_handle').strip()
        # As of December 2020, possibly earlier, the following line no longer works:
        # members = context.original_message.guild.members
        # Instead have to fetch the list from the server, and enable the "SERVER MEMBERS INTENT" permission in the bot admin page on Discord.
        members = await context.original_message.guild.fetch_members(
            limit=1000).flatten()
        for member in members:
            if member.name == target_member_name:
                responseText = '<@{0}>: the snowflake ID for {1} is {2}'.format(
                    context.from_id, target_member_name, member.id)
                return (responseText, None)
        responseText = '<@{0}>: no such member {1}'.format(
            context.from_id, target_member_name)
        return (responseText, None)

    def handleAdminAddEsper(self, context: CommandContextInfo) -> (str, str):
        """Handle !admin-add-esper and !sandbox-admin-add-esper commands to add a new esper to the resonance tracker."""
        sandbox = True
        match = WotvBotConstants.ADMIN_ADD_ESPER_PATTERN.match(
            context.original_message.content)
        if match:
            sandbox = False
        else:
            match = WotvBotConstants.SANDBOX_ADMIN_ADD_ESPER_PATTERN.match(
                context.original_message.content)
        esper_name = match.group('name').strip()
        esper_url = match.group('url').strip()
        left_or_right_of = match.group('left_or_right_of').strip()
        column = match.group('column').strip()
        print(
            'esper add (sandbox mode={6}) from user {0}#{1}, for esper {2}, url {3}, position {4}, column {5}'
            .format(context.from_name, context.from_discrim, esper_name,
                    esper_url, left_or_right_of, column, sandbox))
        context.esper_resonance_manager.addEsperColumn(context.from_id,
                                                       esper_name, esper_url,
                                                       left_or_right_of,
                                                       column, sandbox)
        responseText = '<@{0}>: Added esper {1}!'.format(
            context.from_id, esper_name)
        return (responseText, None)

    def handleAdminAddUnit(self, context: CommandContextInfo) -> (str, str):
        """Handle !admin-add-unit and !sandbox-admin-add-unit commands to add a new unit to the resonance tracker."""
        sandbox = True
        match = WotvBotConstants.ADMIN_ADD_UNIT_PATTERN.match(
            context.original_message.content)
        if match:
            sandbox = False
        else:
            match = WotvBotConstants.SANDBOX_ADMIN_ADD_UNIT_PATTERN.match(
                context.original_message.content)
        unit_name = match.group('name').strip()
        unit_url = match.group('url').strip()
        above_or_below = match.group('above_or_below').strip()
        row1Based = match.group('row1Based').strip()
        print(
            'unit add (sandbox mode={6}) from user {0}#{1}, for unit {2}, url {3}, position {4}, row {5}'
            .format(context.from_name, context.from_discrim, unit_name,
                    unit_url, above_or_below, row1Based, sandbox))
        context.esper_resonance_manager.addUnitRow(context.from_id, unit_name,
                                                   unit_url, above_or_below,
                                                   row1Based, sandbox)
        responseText = '<@{0}>: Added unit {1}!'.format(
            context.from_id, unit_name)
        return (responseText, None)

    def handleAdminAddVisionCard(self,
                                 context: CommandContextInfo) -> (str, str):
        """Handle !admin-add-vc command to add a new vision card."""
        match = WotvBotConstants.ADMIN_ADD_VC_PATTERN.match(
            context.original_message.content)
        card_name = match.group('name').strip()
        card_url = match.group('url').strip()
        above_or_below = match.group('above_or_below').strip()
        row1Based = match.group('row1Based').strip()
        print(
            'vc add from user {0}#{1}, for card {2}, url {3}, position {4}, row {5}'
            .format(context.from_name, context.from_discrim, card_name,
                    card_url, above_or_below, row1Based))
        context.vision_card_manager.addVisionCardRow(context.from_id,
                                                     card_name, card_url,
                                                     above_or_below, row1Based)
        responseText = '<@{0}>: Added card {1}!'.format(
            context.from_id, card_name)
        return (responseText, None)

    def handleAdminAddUser(self, context: CommandContextInfo) -> (str, str):
        """Handle !admin-add-user command to add a new unit to the resonance tracker and the administrative spreadsheet."""
        if not AdminUtils.isAdmin(
                self.wotv_bot_config.spreadsheet_app,
                self.wotv_bot_config.access_control_spreadsheet_id,
                context.from_id):
            raise ExposableException(
                'You do not have permission to add a user.')
        match = WotvBotConstants.ADMIN_ADD_USER_PATTERN.match(
            context.original_message.content)
        snowflake_id = match.group('snowflake_id').strip()
        nickname = match.group('nickname').strip()
        user_type = match.group('user_type').strip().lower()
        is_admin = False
        if user_type == 'admin':
            is_admin = True
        print(
            'user add from user {0}#{1}, for snowflake_id {2}, nickname {3}, is_admin {4}'
            .format(context.from_name, context.from_discrim, snowflake_id,
                    nickname, is_admin))
        AdminUtils.addUser(self.wotv_bot_config.spreadsheet_app,
                           self.wotv_bot_config.access_control_spreadsheet_id,
                           nickname, snowflake_id, is_admin)
        context.esper_resonance_manager.addUser(nickname)
        context.vision_card_manager.addUser(nickname)
        responseText = '<@{0}>: Added user {1}!'.format(
            context.from_id, nickname)
        return (responseText, None)

    async def handleVisionCardDebug(self,
                                    context: CommandContextInfo) -> (str, str):
        """Handle !xocr and !xocr-debug commands to perform OCR on a Vision Card."""
        return await self.handleVisionCardSet(context, is_debug=True)

    async def handleVisionCardSet(self,
                                  context: CommandContextInfo,
                                  is_debug: bool = False) -> (str, str):
        """Handle !vc-set"""
        # Try to extract text from a vision card screenshot that is sent as an attachment to this message.
        url = context.original_message.attachments[0].url
        print('Vision Card OCR request from user %s#%s, for url %s' %
              (context.from_name, context.from_discrim, url))
        screenshot = None
        if self.INTEG_TEST_LOCAL_FILESYSTEM_READ_FOR_VISION_CARD:
            screenshot = VisionCardOcrUtils.loadScreenshotFromFilesystem(url)
        else:
            screenshot = VisionCardOcrUtils.downloadScreenshotFromUrl(url)
        vision_card = VisionCardOcrUtils.extractVisionCardFromScreenshot(
            screenshot, is_debug)
        if is_debug:
            combined_image = VisionCardOcrUtils.mergeDebugImages(vision_card)
            buffer = io.BytesIO()
            combined_image.save(buffer, format='PNG')
            buffer.seek(0)
            temp_file = discord.File(buffer,
                                     filename='Intermediate OCR Debug.png')
            await context.original_message.channel.send(
                'Intermediate OCR Debug. Raw info text:\n```{0}```\nRaw stats text: ```{1}```'
                .format(vision_card.info_debug_raw_text,
                        vision_card.stats_debug_raw_text),
                file=temp_file)
            # Print errors to the console, but do not return them as we cannot guarantee that there is no sensitive
            # information in here, such as possible library exceptions, i/o exceptions, etceteras.
            if vision_card.error_messages is not None and len(
                    vision_card.error_messages) > 0:
                print('errors found during vision card conversion: ' +
                      str(vision_card.error_messages))
        reaction = None
        if vision_card.successfully_extracted is True:
            responseText = '<@{0}>: {1}'.format(context.from_id,
                                                vision_card.prettyPrint())
            if not is_debug:
                context.vision_card_manager.setVisionCard(
                    context.from_id, vision_card)
            reaction = '\U00002705'  # CLDR: check mark button
        else:
            responseText = '<@{0}>: Vision card extraction has failed. You may try again with !vc-debug for a clue about what has gone wrong'.format(
                context.from_id)
        return (responseText, reaction)

    async def handleVisionCardFetchByName(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !vc command for self-lookup of a given vision card by name"""
        target_name = context.command_match.group('target_name').strip()
        print('vision card fetch from user %s#%s, for target %s' %
              (context.from_name, context.from_discrim, target_name))
        vision_card = context.vision_card_manager.readVisionCardByName(
            None, context.from_id, target_name)
        responseText = '<@{0}>: Vision Card:\n{1}'.format(
            context.from_id, str(vision_card.prettyPrint()))
        return (responseText, None)

    async def handleVisionCardAbilitySearch(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !vc-ability command for self-lookup of a given vision card by party/bestowed ability fuzzy-match"""
        search_text = context.command_match.group('search_text').strip()
        print('vision card ability search from user %s#%s, for text %s' %
              (context.from_name, context.from_discrim, search_text))
        vision_cards = context.vision_card_manager.searchVisionCardsByAbility(
            None, context.from_id, search_text)
        if len(vision_cards) == 0:
            responseText = '<@{0}>: No vision cards matched the ability search.'.format(
                context.from_id)
            return (responseText, None)
        responseText = '<@{0}>: Matching Vision Cards:\n'.format(
            context.from_id)
        for vision_card in vision_cards:
            responseText += '  ' + vision_card.Name + '\n'
            responseText += '    Party Ability: ' + vision_card.PartyAbility + '\n'
            for bestowed_effect in vision_card.BestowedEffects:
                responseText += '    Bestowed Effect: ' + bestowed_effect + '\n'
        return (responseText, None)

    @staticmethod
    def rarityAndElementParenthetical(unit: WotvUnit) -> str:
        """Generate a parenthetical string with the unit's rarity and element(s)"""
        text = '(' + str(unit.rarity) + ' rarity, '
        if not unit.elements:
            return text + 'no element)'
        text += unit.elements[0]
        if len(unit.elements) > 1:
            for element in unit.elements[1:]:
                text += '/' + str(element)
        text += ' element'
        if len(unit.elements) > 1:
            text += 's'
        return text + ')'

    def prettyPrintUnitSkillSearchResult(self, result: UnitSkillSearchResult):
        """Print a useful, human-readable description of the skill match including the unit name, element, rarity, the skill name,
           and how the skill is unlocked."""
        if result.is_master_ability:
            return 'Master ability for ' + result.unit.name + ' ' + WotvBot.rarityAndElementParenthetical(
                result.unit) + ': ' + result.skill.description
        if result.is_limit_burst:
            return 'Limit burst (' + result.skill.name + ') for ' + result.unit.name + ' ' + WotvBot.rarityAndElementParenthetical(
                result.unit) + ': ' + result.skill.description
        text = 'Skill "' + result.skill.name + '" learned by ' + result.unit.name
        text += ' ' + WotvBot.rarityAndElementParenthetical(result.unit)
        text += ' with job ' + result.board_skill.unlocked_by_job.name + ' at job level ' + str(
            result.board_skill.unlocked_by_job_level)
        text += ': ' + result.skill.description
        return text

    def prettyPrintUnitJobSearchResult(self, result: UnitJobSearchResult):
        """Print a useful, human-readable description of the job match including the unit name, element, rarity, and job name."""
        text = 'Job "' + result.job.name + '" learned by ' + result.unit.name
        text += ' ' + WotvBot.rarityAndElementParenthetical(result.unit)
        return text

    def prettyPrintUnitSearchResult(self, result: UnitSearchResult):
        """Print a useful, human-readable description of any search result, as appropriate to the type."""
        if hasattr(result, 'is_master_ability'):
            return self.prettyPrintUnitSkillSearchResult(result)
        elif hasattr(result, 'job'):
            return self.prettyPrintUnitJobSearchResult(result)
        else:
            return result.unit.name + ' ' + WotvBot.rarityAndElementParenthetical(
                result.unit)

    @staticmethod
    def getExtraCommandLines(context: CommandContextInfo):
        """Extract all extra non-empty lines from a command and return them as a list."""
        lines = context.original_message.content.splitlines()
        extra_lines = []
        if len(lines) > 1:
            for line in lines[1:]:
                line = line.strip()
                if line:
                    extra_lines.append(line)
        return extra_lines

    # Deprecated - Use rich unit search instead, e.g. "!unit-search skill-name <search_text>"
    async def handleFindSkillsByName(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !skills-by-name command"""
        search_text = context.command_match.group('search_text').strip()
        print('skills-by-name search from user %s#%s, for text %s' %
              (context.from_name, context.from_discrim, search_text))
        refinements = WotvBot.getExtraCommandLines(context)
        if len(refinements) > 0:
            print('  refinements: ' + str(refinements))
        results = DataFileSearchUtils.richUnitSearch(
            self.wotv_bot_config.data_files, 'skill-name', search_text,
            refinements)
        if len(results) == 0:
            responseText = '<@{0}>: No skills matched the search.'.format(
                context.from_id)
            return (responseText, None)
        responseText = '<@{0}>: Matching Skills:\n'.format(context.from_id)
        results = sorted(results, key=lambda one_result: one_result.unit.name)
        truncated = False
        if len(results) > 25:
            results = results[:25]
            truncated = True
        for result in results:
            responseText += self.prettyPrintUnitSearchResult(result) + '\n'
        if truncated:
            responseText += 'Results truncated because there were too many.'
        return (responseText.strip(), None)

    # Deprecated - Use rich unit search instead, e.g. "!unit-search skill-desc <search_text>"
    async def handleFindSkillsByDescription(
            self, context: CommandContextInfo) -> (str, str):
        """Handle !skills-by-desc command"""
        search_text = context.command_match.group('search_text').strip()
        print('skills-by-description search from user %s#%s, for text %s' %
              (context.from_name, context.from_discrim, search_text))
        refinements = WotvBot.getExtraCommandLines(context)
        if len(refinements) > 0:
            print('  refinements: ' + str(refinements))
        results = DataFileSearchUtils.richUnitSearch(
            self.wotv_bot_config.data_files, 'skill-desc', search_text,
            refinements)
        if len(results) == 0:
            responseText = '<@{0}>: No skills matched the search.'.format(
                context.from_id)
            return (responseText, None)
        responseText = '<@{0}>: Matching Skills:\n'.format(context.from_id)
        results = sorted(results, key=lambda one_result: one_result.unit.name)
        truncated = False
        if len(results) > 25:
            results = results[:25]
            truncated = True
        for result in results:
            responseText += self.prettyPrintUnitSearchResult(result) + '\n'
        if truncated:
            responseText += 'Results truncated because there were too many.'
        return (responseText.strip(), None)

    async def handleRichUnitSearch(self,
                                   context: CommandContextInfo) -> (str, str):
        """Handle !unit-search command"""
        search_type = context.command_match.group('search_type').strip()
        search_text = None
        if search_type != 'all':
            search_text = context.command_match.group('search_text').strip()
        print('unit search from user %s#%s, type %s, text %s' %
              (context.from_name, context.from_discrim, search_type,
               search_text))
        refinements = WotvBot.getExtraCommandLines(context)
        if len(refinements) > 0:
            print('  refinements: ' + str(refinements))
        results = DataFileSearchUtils.richUnitSearch(
            self.wotv_bot_config.data_files, search_type, search_text,
            refinements)
        if len(results) == 0:
            responseText = '<@{0}>: No units matched the search.'.format(
                context.from_id)
            return (responseText, None)
        responseText = '<@{0}>: Results:\n'.format(context.from_id)
        results = sorted(results, key=lambda one_result: one_result.unit.name)
        truncated = False
        if len(results) > 25:
            results = results[:25]
            truncated = True
        for result in results:
            responseText += self.prettyPrintUnitSearchResult(result) + '\n'
        if truncated:
            responseText += 'Results truncated because there were too many.'
        return (responseText.strip(), None)

    @staticmethod
    async def whimsyShopNrgReminderCallback(target_channel_id: str,
                                            from_id: str):
        """Handles a reminder callback for a whimsy shop nrg reminder."""
        discord_client: discord.Client = WotvBot.getStaticInstance(
        ).wotv_bot_config.discord_client
        text_channel: discord.TextChannel = discord_client.get_channel(
            target_channel_id)
        #discord_client.loop.create_task(text_channel.send(content = '<@{0}>: This is your requested whimsy shop reminder: NRG spent will now start counting towards the next Whimsy Shop.'.format(from_id)))
        await text_channel.send(
            content=
            '<@{0}>: This is your requested whimsy shop reminder: NRG spent will now start counting towards the next Whimsy Shop.'
            .format(from_id))

    @staticmethod
    async def whimsyShopSpawnReminderCallback(target_channel_id: str,
                                              from_id: str):
        """Handles a reminder callback for a whimsy shop spawn reminder."""
        discord_client: discord.Client = WotvBot.getStaticInstance(
        ).wotv_bot_config.discord_client
        text_channel: discord.TextChannel = discord_client.get_channel(
            target_channel_id)
        #discord_client.loop.create_task(text_channel.send(content = '<@{0}>: This is your requested whimsy shop reminder: The Whimsy Shop is ready to spawn again.'.format(from_id)))
        await text_channel.send(
            content=
            '<@{0}>: This is your requested whimsy shop reminder: The Whimsy Shop is ready to spawn again.'
            .format(from_id))

    async def handleWhimsyReminder(self,
                                   context: CommandContextInfo) -> (str, str):
        """Handle !whimsy command for a whimsy reminder"""
        reminders = self.wotv_bot_config.reminders  # Shorthand
        owner_id = str(context.from_id)  # Shorthand
        command = '<none>'
        if context.command_match.group('command'):
            command = context.command_match.group('command').strip()
        print('Whimsy reminder request from user %s#%s, command %s' %
              (context.from_name, context.from_discrim, command))
        responseText = '<@{0}>: Unknown/unsupported !whimsy command. Use !help for for more information.'.format(
            context.from_id)
        # Default behavior - be smart. If the user has got a reminder set, don't overwrite it unless they pass set-reminder as the command.
        # If they do not have a reminder set, go ahead and set it now.
        append_overwrite_reminder_message = False  # Whether or not to add some reminder text to the message
        if command == '<none>':
            # Check if an existing reminder is set. If so prompt to overwrite...
            if reminders.hasPendingWhimsyNrgReminder(
                    owner_id) or reminders.hasPendingWhimsySpawnReminder(
                        owner_id):
                command = 'when'
                append_overwrite_reminder_message = True  # Remind the user how to overwrite the current timer.
            else:
                command = 'set-reminder'  # Assume the user wants to set a reminder.
        if command == 'set-reminder':
            append_existing_canceled_message = reminders.hasPendingWhimsyNrgReminder(
                owner_id) or reminders.hasPendingWhimsySpawnReminder(owner_id)
            nrg_callback: callable = WotvBot.whimsyShopNrgReminderCallback
            nrg_params = [context.original_message.channel.id, owner_id]
            spawn_callback: callable = WotvBot.whimsyShopSpawnReminderCallback
            spawn_params = nrg_params
            reminders.addWhimsyReminder(
                context.from_name, owner_id, nrg_callback, nrg_params,
                spawn_callback, spawn_params,
                self.whimsy_shop_nrg_reminder_delay_ms,
                self.whimsy_shop_spawn_reminder_delay_ms)
            responseText = '<@{0}>: Your reminder has been set.'.format(
                context.from_id)
            if append_existing_canceled_message:
                responseText += ' Your previous outstanding reminder has been discarded.'
        elif command == 'when':
            if reminders.hasPendingWhimsyNrgReminder(owner_id):
                time_left_minutes = int(
                    reminders.timeTillWhimsyNrgReminder(owner_id) / 60)
                responseText = '<@{0}>: NRG spent will start counting towards the next Whimsy Shop in about {1} minutes.'.format(
                    owner_id, str(time_left_minutes))
                if append_overwrite_reminder_message:
                    responseText += ' To force the timer to reset to 60 minutes *immediately*, use the command "!whimsy set-reminder".'
            elif reminders.hasPendingWhimsySpawnReminder(owner_id):
                time_left_minutes = int(
                    reminders.timeTillWhimsySpawnReminder(owner_id) / 60)
                responseText = '<@{0}>: The Whimsy Shop will be ready to spawn again in about {1} minutes.'.format(
                    owner_id, str(time_left_minutes))
                if append_overwrite_reminder_message:
                    responseText += ' To force the timer to reset to 60 minutes *immediately*, use the command "!whimsy set-reminder".'
            else:
                responseText = '<@{0}>: You do not currently have a whimsy reminder set.'.format(
                    context.from_id)
        elif command == 'cancel':
            reminders.cancelWhimsyReminders(owner_id)
            responseText = '<@{0}>: Any and all outstanding whimsy reminders have been canceled.'.format(
                context.from_id)
        return (responseText, None)

    async def handleRoll(self, context: CommandContextInfo) -> (str, str):
        """Handle !roll command to simulate a dice roll."""
        spec: DiceSpec = DiceSpec.parse(
            context.command_match.group('dice_spec'))
        print('Dice roll request from user %s#%s, spec %s' %
              (context.from_name, context.from_discrim, str(spec)))
        if spec.num_dice > 50:
            responseText = '<@{0}>: Too many dice in !roll command (max 50). Use !help for for more information.'.format(
                context.from_id)
        else:
            results: List[int] = Rolling.rollDice(spec)
            total = 0
            for one_roll in results:
                total += one_roll
            responseText = '<@{0}>: Rolled a total of {1}. Dice values were: {2}'.format(
                context.from_id, str(total), str(results))
        return (responseText.strip(), None)

    async def handlePrediction(self,
                               context: CommandContextInfo) -> (str, str):
        """Handle !predict/astrologize/divine/foretell (etc) command to make a funny prediction."""
        query = context.command_match.group('query')
        print('Prediction request from user %s#%s, query %s' %
              (context.from_name, context.from_discrim, str(query)))
        responseText = '<@{0}>: {1}'.format(context.from_id,
                                            self.predictions.predict(query))
        return (responseText.strip(), None)

    async def handleSchedule(self, context: CommandContextInfo) -> (str, str):
        """Handle a request for the weekly schedule."""
        print('Schedule request from user %s#%s' %
              (context.from_name, context.from_discrim))
        responseText = '<@{0}>:\n{1}'.format(
            context.from_id,
            WeeklyEventSchedule.getDoubleDropRateSchedule('** >> ', ' << **'))
        return (responseText.strip(), None)

    async def handleMats(self, context: CommandContextInfo) -> (str, str):
        """Handle a request for the current double-drop rate room."""
        print('Mats request from user %s#%s' %
              (context.from_name, context.from_discrim))
        responseText = '<@{0}>:\n'.format(context.from_id)
        responseText += 'Today: ' + WeeklyEventSchedule.getTodaysDoubleDropRateEvents(
        ) + '\n'
        responseText += 'Tomorrow: ' + WeeklyEventSchedule.getTomorrowsDoubleDropRateEvents(
        ) + '\n'
        responseText += 'For the full schedule, use !schedule.'
        return (responseText.strip(), None)

    async def createOrResetPeriodicStatusUpdateCallback(self):
        """Create or reset the status update callback for the entire bot."""
        self.wotv_bot_config.reminders.createOrResetPeriodicStatusUpdateCallback(
            WotvBot.periodicStatusUpdateCallback)

    @staticmethod
    async def periodicStatusUpdateCallback():
        """Handles a callback for a periodic status update."""
        bot: WotvBot = WotvBot.getStaticInstance()
        discord_client: discord.Client = bot.wotv_bot_config.discord_client
        new_status = WeeklyEventSchedule.getTodaysDoubleDropRateEvents()
        if bot.last_status is None or bot.last_status != new_status:
            print('Updating bot status to: ' + new_status)
            # Apparently bots cannot use a custom status so gotta stick with a regular one like "Playing" (Game)
            await discord_client.change_presence(activity=discord.Game(
                name=new_status))
            bot.last_status = new_status

    @staticmethod
    async def dailyReminderCallback(target_channel_id: str, from_id: str,
                                    requested_reminders: List[str]):
        """Handles a reminder callback for daily reminders."""
        discord_client: discord.Client = WotvBot.getStaticInstance(
        ).wotv_bot_config.discord_client
        text_channel: discord.TextChannel = discord_client.get_channel(
            target_channel_id)
        reminder_text = '<@{0}>: This is your requested daily reminder. Cancel daily reminders with "!daily-reminders none" or use "!help".'.format(
            from_id)
        if 'mats' in requested_reminders:
            reminder_text += '\n  Today\'s daily double rate drops are: ' + WeeklyEventSchedule.getTodaysDoubleDropRateEvents(
            )
        await text_channel.send(content=reminder_text)

    async def handleDailyReminders(self,
                                   context: CommandContextInfo) -> (str, str):
        """Handle !daily-reminders command for various daily reminders, such as double-drop-rates"""
        reminders = self.wotv_bot_config.reminders  # Shorthand
        owner_id = str(context.from_id)  # Shorthand
        reminder_list_str = '<default>'
        if context.command_match.group('reminder_list'):
            reminder_list_str = context.command_match.group(
                'reminder_list').strip()
        print('Daily reminders request from user %s#%s, reminder list %s' %
              (context.from_name, context.from_discrim, reminder_list_str))
        responseText = '<@{0}>: Unknown/unsupported !daily-reminders command. Use !help for for more information.'.format(
            context.from_id)
        requested_reminders: List[str] = reminder_list_str.split(',')
        configured_reminders_message = '<@{0}>: Your daily reminders have been configured:'.format(
            context.from_id)

        # Default behavior - be smart. If the user has got a reminder set, don't overwrite it unless they pass "none" as the list.
        if reminder_list_str == '<default>':
            if reminders.hasDailyReminder(owner_id):
                responseText = '<@{0}>: You have daily reminders configured. To clear them, use "!daily-reminders none".'.format(
                    context.from_id)
            else:
                responseText = '<@{0}>: You do not currently have daily reminders configured. Use !help for more information.'.format(
                    context.from_id)
        elif reminder_list_str == 'none':
            reminders.cancelDailyReminder(owner_id)
            responseText = '<@{0}>: Your daily reminders have been canceled.'.format(
                context.from_id)
        else:
            added_reminders = []
            if 'mats' in requested_reminders:
                configured_reminders_message += '\n  daily double-drop rate reminder ("mats")'
                added_reminders.append('mats')
            callback: callable = WotvBot.dailyReminderCallback
            callback_params = [
                context.original_message.channel.id, owner_id, added_reminders
            ]
            reminders.addDailyReminder(context.from_name, owner_id, callback,
                                       callback_params)
            responseText = configured_reminders_message
        return (responseText, None)
 def __init__(self,
              league: League,
              num_epochs: int,
              learning_rate: float,
              nn_shape: List[int],
              season: str,
              split: float,
              outfile: str,
              model_dir: str,
              features: List[str],
              cache_dir: str,
              mode: str = DEFAULT_METHOD,
              normalize_weights: bool = False,
              cache_numpy_structures=False,
              predict_next_season=False,
              next_season_csv: str = None):
     """
     Parameters
     ----------
     league: League
         The main League object
     num_epochs: int
         How many epochs to run this configuration for.
     learning_rate: float
         The weight at which to update weights in TensorFlow
     nn_shape: list
         A list of integers corresponding to the NN topology. For example [22, 10] is a 2 layer Network with 22
         and 10 nodes in the layers.
     season: str
         The season or seasons that we are preforming training/testing on
     split: float
         The split at which to divide training/testing data
     outfile: str
         A path to a file, where data about each program execution will be dumped, effectivley serving as a
         history of previous models.
     model_dir: str
         A path to a master directory of models. Individual model directories for various configurations will all
         be placed into this directory
     features: list
         A list of features to use as inputs for the NN or SVM.
     logger: logging
         A logger for this class
     mode: str
         What learning mode we should run the NBAPredictor in for this execution. Options are "DNN" or "SVM"
     normalize_weights: bool
         If set to true, a weight column will be added onto the training and testing examples to boost or downplay
         certain instances
     cache_numpy_structures: bool
         If set to True, the NumPy array from this object will be cached as a pickle object so that if the same
         dataset is used again we do not need to prefrom all the parsing and normalizations.
     predict_next_season: bool
         If set to True, this will not test against existing Data, but instead try to predict the upcoming
         2019-2020 SEASON NBA
      next_season_csv: str
         Path to next seasons CSV file
     """
     self.logger = logging.getLogger(
         f"NBAPredictor.{self.__class__.__name__}")
     self.mode = mode
     self.leauge = league
     self.num_epochs = num_epochs
     self.learning_rate = learning_rate
     self.nn_shape = nn_shape
     self.model_dir = model_dir
     self.normalize_weights = normalize_weights
     self.predict_next_season = predict_next_season
     if self.mode == "SVM":
         self.parsed_season = ReadGames(
             self.leauge,
             season,
             split,
             cache_dir,
             features,
             svm_compat=True,
             normalize_weights=self.normalize_weights,
             cache=cache_numpy_structures)
     else:
         self.parsed_season = ReadGames(self.leauge, season, split, cache_dir, features, svm_compat=False,
                                        normalize_weights=self.normalize_weights,
                                        cache=cache_numpy_structures) if not self.predict_next_season else \
             PredictNextSeason(
             next_season_csv=next_season_csv, leauge=league, season=season, split=split, cache_dir=cache_dir,
             features=features, cache=cache_numpy_structures)
     self.feature_cols = self.create_feature_columns()
     self.model = self.create_model()
     if self.mode == "SVM":
         self.predictions = Predictions(season,
                                        num_epochs,
                                        nn_shape,
                                        self.parsed_season.features,
                                        outfile,
                                        svm_compat=True)
     else:
         self.predictions = Predictions(season,
                                        num_epochs,
                                        nn_shape,
                                        self.parsed_season.features,
                                        outfile,
                                        svm_compat=False)
     self.train_input_function = tf.estimator.inputs.numpy_input_fn(
         x=self.parsed_season.training_features,
         y=self.parsed_season.training_labels,
         batch_size=500,
         num_epochs=None,
         shuffle=False)
     self.test_input_function = tf.estimator.inputs.numpy_input_fn(
         x=self.parsed_season.testing_features,
         y=self.parsed_season.testing_labels,
         num_epochs=1,
         shuffle=False)
     if self.predict_next_season:
         self.predict_input_function = tf.estimator.inputs.numpy_input_fn(
             x=self.parsed_season.next_season_data,
             num_epochs=1,
             shuffle=False)
     if self.mode == "SVM":
         self.logger.info(
             f"Running SVC on the {season} NBA Season(s) for "
             f"{self.num_epochs} epochs with the following input features used: "
             f"{self.parsed_season.features}")
     else:
         self.logger.info(
             f"Running DNN on the {season} NBA Season(s) for"
             f" {self.num_epochs} "
             f"epochs with the following NN shape: {self.nn_shape} and the following input "
             f"features: "
             f"{self.parsed_season.features}")
class TensorflowOperations:
    """
    The TensorflowOperations class is a class that NBAPredictor uses as a facade into various Machine Learning
    libraries such as sklearn and TensorFlow. This module runs our entire training and testing routines before
    passing off control to the Predictions class to organize results data.

    Attributes
    ----------
    mode: str
        What learning mode we should run the NBAPredictor in for this execution. Options are "DNN" or "SVM"
    league: League
        The main League object
    num_epochs: int
        How many epochs to run this configuration for.
    learning_rate: float
        The weight at which to update weights in TensorFlow
    nn_shape: list
        A list of integers corresponding to the NN topology. For example [22, 10] is a 2 layer Network with 22
        and 10 nodes in the layers.
    model_dir: str
        A path to a master directory of models. Individual model directories for various configurations will all
        be placed into this directory
    normalize_weights: bool
        If set to true, a weight column will be added onto the training and testing examples to boost or downplay
        certain instances
    parsed_season: ReadGames
        A ReadGames instance. This object contains all of the training/testing features and labels required to run
        any of the models in TensorflowOperations.
    feature_cols: list
        A list of feature columns for this instance
    model: Tensorflow DNN Classifier or Scikit SVM model.
        An ML model configured according to the parameters requested at object initialization.
    predictions: Predictions
        A predictions object to use after all training/testing has concluded. The Predictions class is in charge of
        post-analysis.
    train_input_function: tf.estimator.inputs.numpy_input_fn
        An input function to use for training
    test_input_function: tf.estimator.inputs.numpy_input_fn
        An input function to use for testing
    logger: logging
        A logger for this class

    Methods
    -------
    create_feature_columns
        Creates a list of feature columns from the features passed in on initialization
    run
        Higher-level function that runs the NBAPredictor for this instance, for the requested number of epochs.
        "Runs" in this instance refers to training, testing, and evaluating.
    train
        In the case of DNN, the model will be trained against the training features in the parsed_season object. For
        SVM strategy, the model will be fit against the training features.
    evaluate
        Used by the TensorFlow DNN in order to evaluate the DNN against the test input function.
    get_predictions
        This is the method tasked with testing against the trained samples. For each game in the testing data,
        make a Home or Away Win/Loss prediction. Predictions are expected to be in the format of a tuple of floats
        such as [0.45, 0.55], where the first index is the odds the home team wins and the second index is the odds
        the away team wins. This list of predictions is passed onto the Predictions object for further post-analysis.
    """
    def __init__(self,
                 league: League,
                 num_epochs: int,
                 learning_rate: float,
                 nn_shape: List[int],
                 season: str,
                 split: float,
                 outfile: str,
                 model_dir: str,
                 features: List[str],
                 cache_dir: str,
                 mode: str = DEFAULT_METHOD,
                 normalize_weights: bool = False,
                 cache_numpy_structures=False,
                 predict_next_season=False,
                 next_season_csv: str = None):
        """
        Parameters
        ----------
        league: League
            The main League object
        num_epochs: int
            How many epochs to run this configuration for.
        learning_rate: float
            The weight at which to update weights in TensorFlow
        nn_shape: list
            A list of integers corresponding to the NN topology. For example [22, 10] is a 2 layer Network with 22
            and 10 nodes in the layers.
        season: str
            The season or seasons that we are preforming training/testing on
        split: float
            The split at which to divide training/testing data
        outfile: str
            A path to a file, where data about each program execution will be dumped, effectivley serving as a
            history of previous models.
        model_dir: str
            A path to a master directory of models. Individual model directories for various configurations will all
            be placed into this directory
        features: list
            A list of features to use as inputs for the NN or SVM.
        logger: logging
            A logger for this class
        mode: str
            What learning mode we should run the NBAPredictor in for this execution. Options are "DNN" or "SVM"
        normalize_weights: bool
            If set to true, a weight column will be added onto the training and testing examples to boost or downplay
            certain instances
        cache_numpy_structures: bool
            If set to True, the NumPy array from this object will be cached as a pickle object so that if the same
            dataset is used again we do not need to prefrom all the parsing and normalizations.
        predict_next_season: bool
            If set to True, this will not test against existing Data, but instead try to predict the upcoming
            2019-2020 SEASON NBA
         next_season_csv: str
            Path to next seasons CSV file
        """
        self.logger = logging.getLogger(
            f"NBAPredictor.{self.__class__.__name__}")
        self.mode = mode
        self.leauge = league
        self.num_epochs = num_epochs
        self.learning_rate = learning_rate
        self.nn_shape = nn_shape
        self.model_dir = model_dir
        self.normalize_weights = normalize_weights
        self.predict_next_season = predict_next_season
        if self.mode == "SVM":
            self.parsed_season = ReadGames(
                self.leauge,
                season,
                split,
                cache_dir,
                features,
                svm_compat=True,
                normalize_weights=self.normalize_weights,
                cache=cache_numpy_structures)
        else:
            self.parsed_season = ReadGames(self.leauge, season, split, cache_dir, features, svm_compat=False,
                                           normalize_weights=self.normalize_weights,
                                           cache=cache_numpy_structures) if not self.predict_next_season else \
                PredictNextSeason(
                next_season_csv=next_season_csv, leauge=league, season=season, split=split, cache_dir=cache_dir,
                features=features, cache=cache_numpy_structures)
        self.feature_cols = self.create_feature_columns()
        self.model = self.create_model()
        if self.mode == "SVM":
            self.predictions = Predictions(season,
                                           num_epochs,
                                           nn_shape,
                                           self.parsed_season.features,
                                           outfile,
                                           svm_compat=True)
        else:
            self.predictions = Predictions(season,
                                           num_epochs,
                                           nn_shape,
                                           self.parsed_season.features,
                                           outfile,
                                           svm_compat=False)
        self.train_input_function = tf.estimator.inputs.numpy_input_fn(
            x=self.parsed_season.training_features,
            y=self.parsed_season.training_labels,
            batch_size=500,
            num_epochs=None,
            shuffle=False)
        self.test_input_function = tf.estimator.inputs.numpy_input_fn(
            x=self.parsed_season.testing_features,
            y=self.parsed_season.testing_labels,
            num_epochs=1,
            shuffle=False)
        if self.predict_next_season:
            self.predict_input_function = tf.estimator.inputs.numpy_input_fn(
                x=self.parsed_season.next_season_data,
                num_epochs=1,
                shuffle=False)
        if self.mode == "SVM":
            self.logger.info(
                f"Running SVC on the {season} NBA Season(s) for "
                f"{self.num_epochs} epochs with the following input features used: "
                f"{self.parsed_season.features}")
        else:
            self.logger.info(
                f"Running DNN on the {season} NBA Season(s) for"
                f" {self.num_epochs} "
                f"epochs with the following NN shape: {self.nn_shape} and the following input "
                f"features: "
                f"{self.parsed_season.features}")

    def create_feature_columns(self) -> List[tf.feature_column.numeric_column]:
        """
        Creates a list of feature columns from the features passed in on initialization

        Returns
        -------
        list
            A list of feature columns for each item in the ReadGame object feature attribute
        """
        return [
            tf.feature_column.numeric_column(key=item)
            for item in self.parsed_season.features
        ]

    def create_model(self) -> Union[tf.estimator.DNNClassifier, svm.SVC]:
        if self.mode == "DNN":
            if self.normalize_weights:
                return tf.estimator.DNNClassifier(
                    model_dir=self.model_dir,
                    hidden_units=self.nn_shape,
                    feature_columns=self.feature_cols,
                    n_classes=2,
                    label_vocabulary=['H', 'A'],
                    weight_column=tf.feature_column.numeric_column('weight'),
                    optimizer=tf.compat.v1.train.ProximalAdagradOptimizer(
                        learning_rate=self.learning_rate,
                        l1_regularization_strength=0.001))

            else:
                return tf.estimator.DNNClassifier(
                    model_dir=self.model_dir,
                    hidden_units=self.nn_shape,
                    feature_columns=self.feature_cols,
                    n_classes=2,
                    label_vocabulary=['H', 'A'],
                    optimizer=tf.compat.v1.train.ProximalAdagradOptimizer(
                        learning_rate=self.learning_rate,
                        l1_regularization_strength=0.001))
        elif self.mode == "SVM":
            return svm.SVC(kernel='rbf')
        else:
            raise RuntimeError(
                f"{self.mode} is not an implemented or recognized ML strategy in NBAPredictor"
            )

    def run(self) -> None:
        """
        Higher-level function that runs the NBAPredictor for this instance, for the requested number of epochs.
        "Runs" in this instance refers to training, testing, and evaluating.

        Returns
        -------
        None
        """
        if self.mode == "DNN":
            if not self.predict_next_season:
                for x in range(0, self.num_epochs):
                    self.logger.info(f"Running instance #{x + 1}")
                    self.train()
                    self.evaluate()
                    self.get_predictions()
            else:
                self.logger.info(f"Predicting NBA Season for 2019-2020")
                for x in range(0, self.num_epochs):
                    self.logger.info(f"Running instance #{x + 1}")
                    self.train()
                    acc = self.evaluate()
                    self.logger.info(
                        f"Accuracy: {float(acc['accuracy']) * 100}")
                    playoffs: Playoffs = self.get_predictions()
                    while playoffs.generate_test_data_for_playoffs():
                        testing_features = playoffs.generate_test_data_for_playoffs(
                        )
                        self.predict_input_function = tf.estimator.inputs.numpy_input_fn(
                            x=testing_features, num_epochs=1, shuffle=False)
                        predictions = list(
                            self.model.predict(
                                input_fn=self.predict_input_function))
                        playoffs.record_playoff_results(predictions)
                    playoffs.log_playoff_results()
                    time.sleep(8)
        elif self.mode == "SVM":
            for x in range(0, self.num_epochs):
                self.logger.info(f"Running instance #{x + 1}")
                self.train()
                self.get_predictions()
        else:
            raise RuntimeError(
                f"{self.mode} is not an implemented or recognized ML strategy in NBAPredictor"
            )
        self.predictions.analyze_end_performance(
        ) if not self.predict_next_season else None

    def train(self) -> None:
        """
        In the case of DNN, the model will be trained against the training features in the parsed_season object. For
        SVM strategy, the model will be fit against the training features.

        Returns
        -------
        None
        """
        if self.mode == "DNN":
            self.model.train(input_fn=self.train_input_function, steps=10)
        elif self.mode == "SVM":
            self.model.fit(self.parsed_season.training_features,
                           self.parsed_season.training_labels)
        else:
            raise RuntimeError(
                f"{self.mode} is not an implemented or recognized ML strategy in NBAPredictor"
            )

    def evaluate(self) -> float:
        """
        Used by the TensorFlow DNN in order to evaluate the DNN against the test input function

        Returns
        -------
        None
        """
        return self.model.evaluate(input_fn=self.test_input_function)

    def get_predictions(self) -> Union[None, Playoffs]:
        """
        This is the method tasked with testing against the trained samples. For each game in the testing data,
        make a Home or Away Win/Loss prediction. Predictions are expected to be in the format of a tuple of floats
        such as [0.45, 0.55], where the first index is the odds the home team wins and the second index is the odds
        the away team wins. This list of predictions is passed onto the Predictions object for further post-analysis

        Returns
        -------
        None
        """
        if self.mode == "DNN":
            if not self.predict_next_season:
                predictions = list(
                    self.model.predict(input_fn=self.test_input_function))
                start_index = self.parsed_season.training_size + 1
                predicted_games = [
                    self.parsed_season.sorted_games[i] for i in range(
                        start_index, len(self.parsed_season.sorted_games))
                ]
                self.predictions.add_dnn_seasonal_prediction_instance(
                    predictions, predicted_games, self.model)
            else:
                next_season_predictions = list(
                    self.model.predict(input_fn=self.predict_input_function))
                return self.parsed_season.analyze_end_of_season_predictions(
                    next_season_predictions)
        elif self.mode == "SVM":
            predictions = self.model.predict(
                self.parsed_season.testing_features)
            start_index = self.parsed_season.training_size + 1
            predicted_games = [
                self.parsed_season.sorted_games[i] for i in range(
                    start_index, len(self.parsed_season.sorted_games))
            ]
            self.predictions.add_svm_seasons_prediction_instance(
                predictions, predicted_games)
        else:
            raise RuntimeError(
                f"{self.mode} is not an implemented or recognized ML strategy in NBAPredictor"
            )
Esempio n. 12
0
from predictions import Predictions
from predictions_valuebets import ValueBets
from volume import Volume

predictions_vanilla = Predictions()
value_bets = ValueBets()
volume_of_bets = Volume()

predictions_vanilla.scrape()
value_bets.scrape()
volume_of_bets.scrape()