Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8a6696823 | ||
|
|
bcdbb7181e | ||
|
|
61f407979e | ||
|
|
839d7f4a51 | ||
|
|
f50cc994d0 | ||
|
|
b1732c07c5 | ||
|
|
ea62bdfc0b | ||
|
|
e2b8f5497c | ||
|
|
31b335b7e8 | ||
|
|
9630657710 | ||
|
|
d1eec25767 | ||
|
|
71f4481372 | ||
|
|
31a98bfb64 | ||
|
|
39d5853638 | ||
|
|
f3939c01dc | ||
|
|
8878cbe305 | ||
|
|
560871a3b3 | ||
|
|
b1e1010e1e | ||
|
|
ee5bd0d838 | ||
|
|
618b006a1e | ||
|
|
216bc91d1a | ||
|
|
3fe2e1bcd5 | ||
|
|
bbdb90c7e2 | ||
|
|
dde606aabf | ||
|
|
830158afaf | ||
|
|
ca8bdb6290 | ||
|
|
9138b143ef | ||
|
|
8dd6a5399a | ||
|
|
86098f26dc | ||
|
|
b54538c40b | ||
|
|
cb8721b6e4 | ||
|
|
c3872730e0 | ||
|
|
e2fd453e28 | ||
|
|
65931fd0dc |
No files matched your search
@@ -2,6 +2,85 @@
|
||||
|
||||
A Python project to analyze and pick lottery numbers using past draw results to predict future draws.
|
||||
|
||||
## Usage
|
||||
|
||||
### Top Level
|
||||
|
||||
```bash
|
||||
usage: main.py [-h] {update,record-count,generate-ticket,predict} ...
|
||||
|
||||
A lottery prediction tool.
|
||||
|
||||
positional arguments:
|
||||
{update,record-count,generate-ticket,predict}
|
||||
update Update database records
|
||||
record-count Get record counts by table in the database
|
||||
generate-ticket Generate a random ticket for the specified game
|
||||
predict Predict the next drawing
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
Scrape the game data from the source site and insert records into the database that are not present. A specific year can
|
||||
be selected as the starting year.
|
||||
|
||||
```bash
|
||||
usage: main.py update [-h] [-y YEAR] [-d]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-y, --year YEAR Year to start updates with
|
||||
-d, --dryrun Run the update process without actually making any database or API calls
|
||||
```
|
||||
|
||||
### Record Count
|
||||
|
||||
Return a count of records in each game table. A date to start the count from can be specified.
|
||||
|
||||
```bash
|
||||
usage: main.py record-count [-h] [-f FROM_DATE]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-f, --from-date FROM_DATE
|
||||
Date from which to base the count on (ex. 2026-01-01). The default value is rule-change for the date when game rules last changed
|
||||
```
|
||||
|
||||
### Generate Ticket
|
||||
|
||||
Generates a random ticket for the game specified.
|
||||
|
||||
```bash
|
||||
usage: main.py generate-ticket [-h] --game {MegaMillions,Powerball}
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--game {MegaMillions,Powerball}
|
||||
The game to generate a random ticket for
|
||||
```
|
||||
|
||||
### Predict
|
||||
|
||||
Due to the lottery drawings being as randomized as possible, generating an accurate prediction is not possible. Many
|
||||
window sizes have been tried during testing and none of them have yielded any hopeful results. The same numbers will
|
||||
most likely be generated with the same window size until the sample changes enough to cause changes in the
|
||||
selections. A different random seed number is chosen for each run to attempt to combat this behavior.
|
||||
|
||||
```bash
|
||||
usage: main.py predict [-h] --game {MegaMillions,Powerball} [--window-size WINDOW_SIZE] [--test]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--game {MegaMillions,Powerball}
|
||||
The game to predict the next result for
|
||||
--window-size WINDOW_SIZE
|
||||
The number of records to use in the prediction model, defaults to 10, use -1 for all records
|
||||
--test Use the most recent drawing as a test subject to find the right window_size
|
||||
```
|
||||
|
||||
## Draw Dates
|
||||
|
||||
Mega Millions drawings are held every Tuesday and Friday. Powerball drawings are held every Monday,
|
||||
@@ -9,7 +88,6 @@ Wednesday, and Friday. In order to conserve processing cycles, the predictor wi
|
||||
if there is the potential for new drawings to collect and save. Powerball can have up to 156/157 drawings
|
||||
and Mega Millions can have up to 104/105 drawings per year, depending on how draw dates fall.
|
||||
|
||||
|
||||
## Storage
|
||||
|
||||
The data will eventually be stored in database tables for faster execution. This will also allow for dynamic
|
||||
@@ -63,6 +141,8 @@ LP_BALL_CONTAINER='li'
|
||||
LP_BALL_CLASS="ball ball"
|
||||
LP_DATE_INPUT_FORMAT="%B %d %Y"
|
||||
LP_DATE_PASS_FORMAT="%m/%d/%Y"
|
||||
LP_JACKPOT_CONTAINER="div"
|
||||
LP_BAR_FORMAT="{l_bar}{bar:30}{r_bar}"
|
||||
|
||||
# Powerball vars
|
||||
LP_POWERBALL_INCEPTION="1992"
|
||||
@@ -90,6 +170,7 @@ LP_DATE_INSERT_FORMAT="%Y-%m-%d"
|
||||
# For pytest clean output
|
||||
PYTHONWARNINGS=ignore
|
||||
```
|
||||
|
||||
## TO-DOs
|
||||
|
||||
1. ~~Review standard tests for sqlalchemy objects~~
|
||||
@@ -97,5 +178,5 @@ PYTHONWARNINGS=ignore
|
||||
3. ~~Create tests with in-memory database~~
|
||||
4. ~~Update local database schema (alembic)~~
|
||||
5. Complete starter code review
|
||||
6. Add code to analyze numbers
|
||||
7. Add code to main.py
|
||||
6. ~~Add code to analyze numbers~~
|
||||
7. ~~Add code to main.py~~
|
||||
+120
-33
@@ -3,13 +3,22 @@
|
||||
|
||||
The base definitions for database access and definition
|
||||
"""
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Sequence, TypeVar
|
||||
|
||||
from sqlalchemy import create_engine, Date, Integer, MetaData, String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
|
||||
from sqlalchemy import (
|
||||
create_engine, Date, func, Integer, MetaData,
|
||||
Row, text,
|
||||
)
|
||||
from sqlalchemy.exc import IntegrityError, PendingRollbackError
|
||||
from sqlalchemy.orm import (
|
||||
DeclarativeBase, Mapped, mapped_column, Session, sessionmaker,
|
||||
)
|
||||
from sqlalchemy_utils import create_database, database_exists
|
||||
|
||||
from util.environment import load_environment_variables
|
||||
|
||||
|
||||
project_variables = load_environment_variables()
|
||||
|
||||
"""
|
||||
@@ -28,24 +37,12 @@ DATABASE_NAMING_CONVENTION = {
|
||||
"pk": "pk_%(table_name)s",
|
||||
}
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
metadata = MetaData(naming_convention=DATABASE_NAMING_CONVENTION)
|
||||
|
||||
|
||||
class DataAccessLayer:
|
||||
|
||||
def __init__(self):
|
||||
self.engine = None
|
||||
self.conn_string = project_variables["LP_DATABASE_URL"]
|
||||
|
||||
|
||||
def connect(self):
|
||||
self.engine = create_engine(self.conn_string)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
|
||||
dal = DataAccessLayer()
|
||||
Table = TypeVar("Table", bound=Base)
|
||||
|
||||
|
||||
class PowerballDraw(Base):
|
||||
@@ -62,18 +59,27 @@ class PowerballDraw(Base):
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"PowerballDraw({self.draw_date=}, {self.main_ball1=}, {self.main_ball2=}, {self.main_ball3=}, " \
|
||||
f"{self.main_ball4=}, {self.main_ball5=}, {self.powerball=}, {self.power_play=})"
|
||||
return (f"PowerballDraw({self.draw_date=}, {self.main_ball1=}, "
|
||||
f"{self.main_ball2=}, {self.main_ball3=}, {self.main_ball4=}, "
|
||||
f"{self.main_ball5=}, {self.powerball=}, {self.power_play=})")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.draw_date}, [{self.main_ball1}, {self.main_ball2}, {self.main_ball3}, {self.main_ball4}, " \
|
||||
f"{self.main_ball5}], {self.powerball}, {self.power_play if self.power_play else 1}x"
|
||||
return (f"{self.draw_date}, [{self.main_ball1}, {self.main_ball2}, "
|
||||
f"{self.main_ball3}, {self.main_ball4}, {self.main_ball5}], "
|
||||
f"{self.powerball}, "
|
||||
f"{self.power_play if self.power_play else 1}x")
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.draw_date == other.draw_date and self.main_ball1 == other.main_ball1 and \
|
||||
self.main_ball2 == other.main_ball2 and self.main_ball3 == other.main_ball3 and \
|
||||
self.main_ball4 == other.main_ball4 and self.main_ball5 == other.main_ball5 and \
|
||||
self.powerball == other.powerball and self.power_play == other.power_play
|
||||
return (self.draw_date == other.draw_date and self.main_ball1 ==
|
||||
other.main_ball1 and \
|
||||
self.main_ball2 == other.main_ball2 and self.main_ball3 ==
|
||||
other.main_ball3 and \
|
||||
self.main_ball4 == other.main_ball4 and self.main_ball5 ==
|
||||
other.main_ball5 and \
|
||||
self.powerball == other.powerball and self.power_play ==
|
||||
other.power_play)
|
||||
|
||||
|
||||
class MegaMillionsDraw(Base):
|
||||
@@ -90,15 +96,96 @@ class MegaMillionsDraw(Base):
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"MegaMillionsDraw({self.draw_date=}, {self.main_ball1=}, {self.main_ball2=}, {self.main_ball3=}, " \
|
||||
f"{self.main_ball4=}, {self.main_ball5=}, {self.mega_ball=}, {self.megaplier=})"
|
||||
return (f"MegaMillionsDraw({self.draw_date=}, {self.main_ball1=}, "
|
||||
f"{self.main_ball2=}, {self.main_ball3=}, "
|
||||
f"{self.main_ball4=}, {self.main_ball5=}, "
|
||||
f"{self.mega_ball=}, {self.megaplier=})")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.draw_date}, [{self.main_ball1}, {self.main_ball2}, {self.main_ball3}, {self.main_ball4}, " \
|
||||
f"{self.main_ball5}], {self.mega_ball}, {self.megaplier if self.megaplier else 1}x"
|
||||
return (f"{self.draw_date}, [{self.main_ball1}, {self.main_ball2}, "
|
||||
f"{self.main_ball3}, {self.main_ball4}, "
|
||||
f"{self.main_ball5}], {self.mega_ball}, "
|
||||
f"{self.megaplier if self.megaplier else 1}x")
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.draw_date == other.draw_date and self.main_ball1 == other.main_ball1 and \
|
||||
self.main_ball2 == other.main_ball2 and self.main_ball3 == other.main_ball3 and \
|
||||
self.main_ball4 == other.main_ball4 and self.main_ball5 == other.main_ball5 and \
|
||||
self.mega_ball == other.mega_ball and self.megaplier == other.megaplier
|
||||
return (self.draw_date == other.draw_date and self.main_ball1 ==
|
||||
other.main_ball1 and \
|
||||
self.main_ball2 == other.main_ball2 and self.main_ball3 ==
|
||||
other.main_ball3 and \
|
||||
self.main_ball4 == other.main_ball4 and self.main_ball5 ==
|
||||
other.main_ball5 and \
|
||||
self.mega_ball == other.mega_ball and self.megaplier ==
|
||||
other.megaplier)
|
||||
|
||||
|
||||
class DataAccessLayer:
|
||||
|
||||
def __init__(self):
|
||||
self.engine = None
|
||||
self.conn_string = project_variables["LP_DATABASE_URL"]
|
||||
|
||||
|
||||
def connect(self):
|
||||
self.engine = create_engine(self.conn_string)
|
||||
if not database_exists(self.engine.url):
|
||||
create_database(self.engine.url)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
|
||||
class DataAccessLayer2:
|
||||
|
||||
def __init__(self):
|
||||
self.engine = create_engine(
|
||||
project_variables["LP_DATABASE_URL"], echo=False,
|
||||
)
|
||||
if not database_exists(self.engine.url):
|
||||
create_database(self.engine.url)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session_local: sessionmaker[Session] = sessionmaker(
|
||||
bind=self.engine,
|
||||
)
|
||||
self.session_factory = self.session_local
|
||||
|
||||
|
||||
def execute_query(self, query) -> Sequence[Row[Any]]:
|
||||
with self.session_factory() as session:
|
||||
try:
|
||||
result = session.execute(text(query))
|
||||
return result.fetchall()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def add(self, record: Table) -> None:
|
||||
with self.session_factory() as session:
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
except PendingRollbackError:
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def count(self, table_name: str, from_date: date | None = None) -> int:
|
||||
with self.session_factory() as session:
|
||||
if from_date:
|
||||
return session.query(table_name).where(
|
||||
table_name.draw_date >= from_date,
|
||||
).count()
|
||||
else:
|
||||
return session.query(table_name).count()
|
||||
|
||||
|
||||
def most_recent(self, table_name: Table) -> Row[Table] | None:
|
||||
with self.session_factory() as session:
|
||||
return session.query(func.max(table_name.draw_date)).first()
|
||||
|
||||
|
||||
dal = DataAccessLayer()
|
||||
dal2 = DataAccessLayer2()
|
||||
+136
-27
@@ -1,22 +1,22 @@
|
||||
from datetime import datetime
|
||||
import random
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.ensemble import RandomForestRegressor
|
||||
from sqlalchemy import select
|
||||
|
||||
from data.database import Base, dal, project_variables
|
||||
import util.drawing as drawutil
|
||||
|
||||
RANDOM_SEED = 42
|
||||
from data.database import Base, dal
|
||||
from lottery_predictor.config import GAME_INFO
|
||||
|
||||
|
||||
def load_dataframe(
|
||||
table_name: str, start_date: datetime | None = None, end_date: datetime | None = None
|
||||
def load_dataframe_by_dates(
|
||||
game: str, start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
:param table_name: the name of the table to load data from
|
||||
:param game: the name of the game to get data for
|
||||
:param start_date: the start date for the data
|
||||
:param end_date: the end date for the data
|
||||
|
||||
@@ -26,17 +26,26 @@ def load_dataframe(
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
|
||||
game_dict = drawutil.check_table_name(table_name=table_name)
|
||||
game_dict = GAME_INFO[game]
|
||||
if game_dict:
|
||||
target_table = Base.metadata.tables.get(game_dict['db_table_name'])
|
||||
if start_date is not None and end_date is not None:
|
||||
sql_statement = (select(target_table)
|
||||
.where(target_table.columns.draw_date >= start_date)
|
||||
.where(target_table.columns.draw_date <= end_date))
|
||||
.where(
|
||||
target_table.columns.draw_date >= start_date,
|
||||
)
|
||||
.where(target_table.columns.draw_date <= end_date)
|
||||
.order_by(target_table.columns.draw_date.desc()))
|
||||
elif start_date is not None and end_date is None:
|
||||
sql_statement = (select(target_table).where(target_table.columns.draw_date >= start_date))
|
||||
sql_statement = (select(target_table)
|
||||
.where(
|
||||
target_table.columns.draw_date >= start_date,
|
||||
)
|
||||
.order_by(target_table.columns.draw_date.desc()))
|
||||
elif start_date is None and end_date is not None:
|
||||
sql_statement = (select(target_table).where(target_table.columns.draw_date <= end_date))
|
||||
sql_statement = (select(target_table)
|
||||
.where(target_table.columns.draw_date <= end_date)
|
||||
.order_by(target_table.columns.draw_date.desc()))
|
||||
else:
|
||||
sql_statement = select(target_table)
|
||||
|
||||
@@ -45,8 +54,93 @@ def load_dataframe(
|
||||
raise ValueError('An invalid table name was provided')
|
||||
|
||||
|
||||
def load_dataframe_most_recent(game: str, limit: int = 10) -> pd.DataFrame:
|
||||
"""
|
||||
:param game: the name of the game to get data for
|
||||
:param limit: The N most recent draws (default: 10)
|
||||
|
||||
:returns: a pandas DataFrame containing the data
|
||||
"""
|
||||
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
|
||||
game_dict = GAME_INFO[game]
|
||||
if game_dict:
|
||||
target_table = Base.metadata.tables.get(game_dict['db_table_name'])
|
||||
if limit is not None and limit > 0:
|
||||
sql_statement = (select(target_table)
|
||||
.order_by(target_table.columns.draw_date.desc())
|
||||
.limit(limit))
|
||||
return pd.read_sql(sql_statement, session.bind)
|
||||
else:
|
||||
raise ValueError(
|
||||
'Limit must be a positive integer greater than zero.',
|
||||
)
|
||||
else:
|
||||
raise ValueError('An invalid table name was provided.')
|
||||
|
||||
|
||||
def prepare_split_data(data: np.ndarray, window_size: int = 10) -> tuple[
|
||||
np.ndarray, np.ndarray, np.ndarray]:
|
||||
# Clean the data by removing the draw_date and multiplier columns
|
||||
clean_data = data[:, 1:7]
|
||||
|
||||
# Check to be sure there is enough data for the window_size
|
||||
if len(clean_data) <= window_size:
|
||||
raise ValueError(
|
||||
f"Not enough data! Dataset has {len(clean_data)} rows, "
|
||||
f"but window_size requires at least {window_size + 1} rows.",
|
||||
)
|
||||
|
||||
# Calculate the indices for all windows at once
|
||||
indices = np.arange(len(clean_data) - window_size)
|
||||
|
||||
# Create X: flattened sliding windows
|
||||
x = np.array([clean_data[i: i + window_size].flatten() for i in indices])
|
||||
|
||||
# Create y_field: first 5 columns (2D array)
|
||||
y_field = clean_data[window_size:, 0:5]
|
||||
|
||||
# Create y_game on the last column ensuring it is a 2D array
|
||||
y_game = clean_data[window_size:, 5].ravel()
|
||||
|
||||
return x, y_field, y_game
|
||||
|
||||
|
||||
def make_prediction(data_frame: pd.DataFrame, window_size: int = 10) -> tuple[
|
||||
np.ndarray, np.ndarray]:
|
||||
# Get a random number
|
||||
state = random.randint(1000, 300_000)
|
||||
|
||||
# Convert the data_frame to a numpy array for slicing
|
||||
data = data_frame.values
|
||||
|
||||
# Prepare and split the data
|
||||
x, y_field, y_game = prepare_split_data(data, window_size=window_size)
|
||||
|
||||
# Train the model for the field balls
|
||||
field_model = RandomForestRegressor(n_estimators=200, random_state=state)
|
||||
field_model.fit(x, y_field)
|
||||
|
||||
# Train the model for the game ball
|
||||
game_model = RandomForestRegressor(n_estimators=200, random_state=state)
|
||||
game_model.fit(x, y_game)
|
||||
|
||||
# Predict the next draw
|
||||
clean_data = data[:, 1:7]
|
||||
current_window = clean_data[-window_size:].flatten().reshape(1, -1)
|
||||
# Get predictions and round to the nearest whole number
|
||||
predicted_field = np.sort(
|
||||
np.round(field_model.predict(current_window)).astype(int),
|
||||
)
|
||||
predicted_game = np.round(game_model.predict(current_window)).astype(int)
|
||||
|
||||
return predicted_field[0], predicted_game[0]
|
||||
|
||||
|
||||
def get_most_common_number(
|
||||
data_frame: pd.DataFrame, columns: list | None = None, top: int = 1
|
||||
data_frame: pd.DataFrame, columns: list | None = None, top: int = 1,
|
||||
) -> list[int]:
|
||||
"""
|
||||
:param data_frame: a pandas DataFrame containing the data
|
||||
@@ -56,14 +150,16 @@ def get_most_common_number(
|
||||
:returns: a list of the most common numbers
|
||||
"""
|
||||
if columns is None:
|
||||
columns = ['main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5']
|
||||
columns = [
|
||||
'main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5',
|
||||
]
|
||||
flat_numbers = data_frame[columns].values.flatten()
|
||||
counts = Counter(flat_numbers)
|
||||
return [int(num) for num, _ in counts.most_common(top)]
|
||||
|
||||
|
||||
def get_least_common_number(
|
||||
data_frame: pd.DataFrame, columns: list | None = None, bottom: int = 1
|
||||
data_frame: pd.DataFrame, columns: list | None = None, bottom: int = 1,
|
||||
) -> list[int]:
|
||||
"""
|
||||
:param data_frame: a pandas DataFrame containing the data
|
||||
@@ -73,23 +169,30 @@ def get_least_common_number(
|
||||
:returns: a list of the least common numbers
|
||||
"""
|
||||
if columns is None:
|
||||
columns = ['main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5']
|
||||
columns = [
|
||||
'main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5',
|
||||
]
|
||||
flat_numbers = data_frame[columns].values.flatten()
|
||||
counts = Counter(flat_numbers)
|
||||
return list(reversed([int(num) for num, _ in counts.most_common()[-bottom:]]))
|
||||
return list(
|
||||
reversed([int(num) for num, _ in counts.most_common()[-bottom:]]),
|
||||
)
|
||||
|
||||
|
||||
def calculate_probabilities(
|
||||
data_frame: pd.DataFrame, max_number: int, columns: list | None = None
|
||||
data_frame: pd.DataFrame, max_number: int, columns: list | None = None,
|
||||
) -> dict[int, float]:
|
||||
"""
|
||||
:param data_frame: A pandas DataFrame containing the data to calculate probabilities for
|
||||
:param data_frame: A pandas DataFrame containing the data to calculate
|
||||
probabilities for
|
||||
:param max_number: The maximum number possible in the data_frame
|
||||
:param columns: The list of column names to use from the data_frame
|
||||
:returns dict: A dictionary containing the probabilities
|
||||
"""
|
||||
if columns is None:
|
||||
columns = ['main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5']
|
||||
columns = [
|
||||
'main_ball1', 'main_ball2', 'main_ball3', 'main_ball4', 'main_ball5',
|
||||
]
|
||||
if data_frame.empty:
|
||||
return dict()
|
||||
|
||||
@@ -98,9 +201,11 @@ def calculate_probabilities(
|
||||
# count all the occurrences of each number
|
||||
counts = Counter(all_numbers)
|
||||
# calculate the basic probability of each number occurring again
|
||||
probabilities = {num: counts.get(num, 0) / (max_number + 1) for num in range(1, max_number + 1)}
|
||||
probabilities = {num: counts.get(num, 0) / (max_number + 1) for num in
|
||||
range(1, max_number + 1)}
|
||||
|
||||
# if any calculation is greater than one, use what is to the right of the decimal point as the value
|
||||
# if any calculation is greater than one, use what is to the right of the
|
||||
# decimal point as the value
|
||||
for key, value in probabilities.items():
|
||||
if value > 1:
|
||||
probabilities[key] = value - int(str(value).split('.')[0])
|
||||
@@ -109,16 +214,20 @@ def calculate_probabilities(
|
||||
return probabilities
|
||||
|
||||
|
||||
def get_hot_numbers(probabilities: dict[int, float], top: int = 5) -> list[tuple[int, float]]:
|
||||
def get_hot_numbers(probabilities: dict[int, float], top: int = 5) -> list[
|
||||
tuple[int, float]]:
|
||||
"""
|
||||
:param probabilities: A dictionary containing the probabilities
|
||||
:param top: The count of hottest items to return, defaults to 5
|
||||
:returns list of tuples: A list of the hot numbers and their raw score
|
||||
"""
|
||||
return sorted(probabilities.items(), key=lambda item: item[1], reverse=True)[:top]
|
||||
return sorted(
|
||||
probabilities.items(), key=lambda item: item[1], reverse=True,
|
||||
)[:top]
|
||||
|
||||
|
||||
def get_cold_numbers(probabilities: dict[int, float], bottom: int = 5) -> list[tuple[int, float]]:
|
||||
def get_cold_numbers(probabilities: dict[int, float], bottom: int = 5) -> list[
|
||||
tuple[int, float]]:
|
||||
"""
|
||||
:param probabilities: A dictionary containing the probabilities
|
||||
:param bottom: The count of coldest items to return, defaults to 5
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
collect.py
|
||||
|
||||
Functions for collecting data for insertion into the databse or pulling data from the database for analysis
|
||||
Functions for collecting data for insertion into the databse or pulling
|
||||
data from the database for analysis
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
@@ -9,80 +10,99 @@ from time import sleep
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from data.database import Base, dal
|
||||
import util.drawing as drawutil
|
||||
import util.environment as envutil
|
||||
import util.scrape as scrapeutil
|
||||
|
||||
project_variables = envutil.load_environment_variables()
|
||||
from data.database import Base, dal
|
||||
from lottery_predictor.config import GAME_INFO, PROJECT_VARIABLES
|
||||
|
||||
|
||||
def get_record_count(table_name: str) -> int | None:
|
||||
def get_record_count(game: str) -> int | None:
|
||||
"""
|
||||
:param table_name: the name of the table to get a count of records from
|
||||
:param game: the name of the game to get a count of records for
|
||||
|
||||
:returns: the count of records found in the specified table
|
||||
"""
|
||||
game_dict = drawutil.check_table_name(table_name=table_name)
|
||||
game_dict = GAME_INFO[game]
|
||||
if game_dict:
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
target_table = Base.metadata.tables.get(game_dict['db_table_name'])
|
||||
return session.query(func.count(target_table.columns.draw_date)).first()[0]
|
||||
return \
|
||||
session.query(func.count(target_table.columns.draw_date)).first()[0]
|
||||
else:
|
||||
raise ValueError('An invalid table name was specified.')
|
||||
|
||||
|
||||
def get_recent_draw_date(table_name: str) -> datetime | None:
|
||||
def get_recent_draw_date(game: str) -> datetime | None:
|
||||
"""
|
||||
:param table_name: the name of the table to get the most recent draw date from
|
||||
:param game: the name of the game to get a recent draw date for
|
||||
|
||||
:returns datetime: the most recent draw date, if there is one
|
||||
"""
|
||||
game_dict = drawutil.check_table_name(table_name=table_name)
|
||||
game_dict = GAME_INFO[game]
|
||||
if game_dict:
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
target_table = Base.metadata.tables.get(game_dict['db_table_name'])
|
||||
return session.query(func.max(target_table.columns.draw_date)).first()[0]
|
||||
return session.query(func.max(target_table.columns.draw_date)).first()[
|
||||
0]
|
||||
else:
|
||||
raise ValueError('An invalid table name was specified.')
|
||||
|
||||
|
||||
def get_missing_records(table_name: str) -> int:
|
||||
def get_missing_records(game: str) -> int:
|
||||
"""
|
||||
:param table_name: the name of the table to collect and insert missing records in
|
||||
:param game: the name of the game to get missing records for
|
||||
|
||||
:returns: the number of records inserted into the target table
|
||||
"""
|
||||
# if the table_name is valid
|
||||
game_dict = drawutil.check_table_name(table_name=table_name)
|
||||
# if the game is valid
|
||||
game_dict = GAME_INFO[game]
|
||||
if game_dict:
|
||||
insert_count = 0
|
||||
# connect to the database and establish a session
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
|
||||
date_format = project_variables['LP_DATE_PASS_FORMAT']
|
||||
date_format = PROJECT_VARIABLES['LP_DATE_PASS_FORMAT']
|
||||
|
||||
target_table = Base.metadata.tables.get(game_dict['db_table_name'])
|
||||
record_count = get_record_count(table_name=table_name)
|
||||
record_count = get_record_count(game=game)
|
||||
target_game = game_dict['game_name']
|
||||
end_year = datetime.today().year + 1
|
||||
recent_draw_date = (get_recent_draw_date(table_name=table_name) + timedelta(days=1)).strftime(date_format)
|
||||
following_draw_date = datetime.strptime(drawutil.get_next_draw_date(game=target_game,
|
||||
from_date=recent_draw_date), date_format)
|
||||
start_year = game_dict['inception_year'] if record_count == 0 else following_draw_date.year
|
||||
check_date = datetime.strptime(drawutil.get_previous_draw_date(game=target_game, from_date=
|
||||
drawutil.get_next_draw_date(game=target_game)), date_format).date()
|
||||
recent_draw_date = (get_recent_draw_date(game=game) + timedelta(
|
||||
days=1,
|
||||
)).strftime(
|
||||
date_format,
|
||||
)
|
||||
following_draw_date = datetime.strptime(
|
||||
drawutil.get_next_draw_date(
|
||||
game=target_game,
|
||||
from_date=recent_draw_date,
|
||||
), date_format,
|
||||
)
|
||||
start_year = game_dict[
|
||||
'inception_year'] if record_count == 0 else following_draw_date.year
|
||||
check_date = datetime.strptime(
|
||||
drawutil.get_previous_draw_date(
|
||||
game=target_game, from_date=
|
||||
drawutil.get_next_draw_date(game=target_game),
|
||||
), date_format,
|
||||
).date()
|
||||
|
||||
if session.query(target_table).filter_by(draw_date=check_date).first() is None:
|
||||
if session.query(target_table).filter_by(
|
||||
draw_date=check_date,
|
||||
).first() is None:
|
||||
# loop over the range of years and insert missing records
|
||||
for year in range(start_year, end_year):
|
||||
scrape_data = scrapeutil.scrape_game_history(game=target_game, year=year)
|
||||
scrape_data = scrapeutil.scrape_game_history(
|
||||
game=target_game, year=year,
|
||||
)
|
||||
# convert and insert records, discarding duplicate dates
|
||||
for draw_date, draw_info in scrape_data.items():
|
||||
record = game_dict['record_type'](draw_date=draw_date, draw_result=draw_info)
|
||||
record = game_dict['record_type'](
|
||||
draw_date=draw_date, draw_result=draw_info,
|
||||
)
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
@@ -90,7 +110,10 @@ def get_missing_records(table_name: str) -> int:
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
if year != end_year - 1:
|
||||
sleep(30) # sleep between calls to the site for data to avoid being blocked
|
||||
sleep(
|
||||
30,
|
||||
) # sleep between calls to the site for data to
|
||||
# avoid being blocked
|
||||
|
||||
return insert_count
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from data.database import MegaMillionsDraw, PowerballDraw
|
||||
from util.drawing import get_mega_millions_object, get_powerball_object
|
||||
from util.environment import load_environment_variables
|
||||
from util.scrape import project_variables
|
||||
|
||||
|
||||
PROJECT_VARIABLES = load_environment_variables()
|
||||
|
||||
RANDOM_SEED = 42
|
||||
GAME_INFO = {
|
||||
'MegaMillions': {
|
||||
'game_name': 'mega-millions',
|
||||
'table': MegaMillionsDraw,
|
||||
'db_table_name': 'mega_millions_draws',
|
||||
'inception_year': int(project_variables["LP_MEGA_MILLIONS_INCEPTION"]),
|
||||
'rule_change': project_variables["LP_MEGA_MILLIONS_RULE_CHANGE_DATE"],
|
||||
'max_game_ball': int(
|
||||
project_variables["LP_MEGA_MILLIONS_MAX_MEGA_BALL"],
|
||||
),
|
||||
'max_main_ball': int(
|
||||
project_variables["LP_MEGA_MILLIONS_MAX_MAIN_BALL"],
|
||||
),
|
||||
'main_count': 5,
|
||||
'game_ball_name': 'Mega Ball',
|
||||
'scrape_name': 'mega-millions',
|
||||
'object_function': get_mega_millions_object,
|
||||
},
|
||||
'Powerball': {
|
||||
'game_name': 'powerball',
|
||||
'table': PowerballDraw,
|
||||
'db_table_name': 'powerball_draws',
|
||||
'inception_year': int(project_variables["LP_POWERBALL_INCEPTION"]),
|
||||
'rule_change': project_variables["LP_POWERBALL_RULE_CHANGE_DATE"],
|
||||
'max_game_ball': int(project_variables["LP_POWERBALL_MAX_MAIN_BALL"]),
|
||||
'max_main_ball': int(project_variables["LP_POWERBALL_MAX_POWERBALL"]),
|
||||
'main_count': 5,
|
||||
'game_ball_name': 'Powerball',
|
||||
'scrape_name': 'powerball',
|
||||
'object_function': get_powerball_object,
|
||||
},
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import random
|
||||
|
||||
|
||||
def generate_random_ticket(max_main: int, max_game: int, num_main: int = 5) -> tuple[list[int], int]:
|
||||
def generate_random_ticket(max_main: int, max_game: int, num_main: int = 5) -> \
|
||||
tuple[list[int], int]:
|
||||
"""
|
||||
Generate a random lottery ticket.
|
||||
|
||||
:param max_main: The maximum number in the range to choose from for main game balls
|
||||
:param max_game: The maximum number in the range to choose from for the game ball
|
||||
:param max_main: The maximum number in the range to choose from for main
|
||||
game balls
|
||||
:param max_game: The maximum number in the range to choose from for the
|
||||
game ball
|
||||
:param num_main: The number of numbers that can be chosen in the main field
|
||||
|
||||
:returns: A tuple of (main_balls, game_ball)
|
||||
"""
|
||||
main_balls = random.sample(range(1, max_main), num_main)
|
||||
game_ball = random.randint(1, max_game)
|
||||
main_balls = random.sample(range(1, max_main + 1), num_main)
|
||||
game_ball = random.randint(1, max_game + 1)
|
||||
return sorted(main_balls), game_ball
|
||||
@@ -1,116 +1,365 @@
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from time import sleep
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from tqdm import tqdm
|
||||
|
||||
from data.database import dal, MegaMillionsDraw, PowerballDraw
|
||||
import lottery_predictor.collect as predictor
|
||||
from util.drawing import get_mega_millions_object, get_powerball_object
|
||||
from util.environment import load_environment_variables
|
||||
import lottery_predictor.analyze as analyze
|
||||
import lottery_predictor.draw as ticket
|
||||
from data.database import dal2
|
||||
from lottery_predictor.config import GAME_INFO, PROJECT_VARIABLES
|
||||
from util.scrape import scrape_game_history
|
||||
|
||||
project_variables = load_environment_variables()
|
||||
|
||||
BAR_FORMAT = PROJECT_VARIABLES["LP_BAR_FORMAT"]
|
||||
|
||||
|
||||
def dry_run_fill_game_data(game: str, year: int | None = None) -> None:
|
||||
"""
|
||||
|
||||
:param game: Name of the game to process
|
||||
:param year: A starting year or None
|
||||
:return: Nothing
|
||||
|
||||
A dummy job to go through the motions of updating without doing anything
|
||||
"""
|
||||
if year is None:
|
||||
year = GAME_INFO[game]['start_year']
|
||||
elif not isinstance(year, int):
|
||||
raise ValueError(f"Year must be a valid year or None, received {year}")
|
||||
elif year not in range(1992, 2030):
|
||||
raise ValueError(
|
||||
f"Year must be within the range 1992 and 2030, received {year}",
|
||||
)
|
||||
|
||||
for year in range(year, datetime.today().year + 1):
|
||||
for _ in tqdm(
|
||||
range(random.randint(1, 106)),
|
||||
desc=f"\tProcessing year {year}",
|
||||
bar_format=BAR_FORMAT,
|
||||
):
|
||||
sleep(0.25)
|
||||
|
||||
if year != datetime.now().year:
|
||||
for _ in tqdm(
|
||||
range(5),
|
||||
desc='\tPseudo API backoff',
|
||||
bar_format=BAR_FORMAT,
|
||||
):
|
||||
sleep(1)
|
||||
else:
|
||||
print()
|
||||
|
||||
|
||||
def fill_game_data(game: str, year: int | None = None) -> None:
|
||||
"""
|
||||
:param game: Name of the game to process
|
||||
:param year: A starting year or None
|
||||
:return: Nothing
|
||||
|
||||
If the year is None, all game data will be scraped for each year the game
|
||||
has data available.
|
||||
Otherwise, the scrape will be limited to a range of years starting with
|
||||
the year specified
|
||||
Not all data scraped will be used for predictions, but could be useful
|
||||
later.
|
||||
"""
|
||||
if year is None:
|
||||
year = GAME_INFO[game]['start_year']
|
||||
elif not isinstance(year, int):
|
||||
raise ValueError(f"Year must be a valid year or None, received {year}")
|
||||
elif year not in range(1992, 2030):
|
||||
raise ValueError(
|
||||
f"Year must be within the range 1992 and 2030, received {year}",
|
||||
)
|
||||
|
||||
# scrape records by year and insert into the database
|
||||
for year in range(year, datetime.today().year + 1):
|
||||
scrape_data = scrape_game_history(
|
||||
game=GAME_INFO[game]['scrape_name'], year=year,
|
||||
)
|
||||
# convert and insert records
|
||||
for _ in tqdm(
|
||||
range(len(scrape_data)),
|
||||
desc=f"Processing year {year}",
|
||||
bar_format=BAR_FORMAT,
|
||||
):
|
||||
for draw_date, numbers in scrape_data.items():
|
||||
record = GAME_INFO[game]['object_function'](
|
||||
draw_date=draw_date, draw_result=numbers,
|
||||
)
|
||||
dal2.add(record)
|
||||
# sleep for 30 seconds to avoid overtaxing the endpoint, if the
|
||||
# processed year is not the current one
|
||||
if year != datetime.now().year:
|
||||
for _ in tqdm(
|
||||
range(30),
|
||||
desc='\tAPI backoff',
|
||||
bar_format=BAR_FORMAT,
|
||||
):
|
||||
sleep(1)
|
||||
|
||||
|
||||
def update_games(dry_run: bool = False) -> None:
|
||||
"""
|
||||
:return: None
|
||||
|
||||
Loop over games in the game_info dictionary and update the stored records
|
||||
"""
|
||||
date_format = PROJECT_VARIABLES["LP_DATE_INSERT_FORMAT"]
|
||||
|
||||
for game, info in GAME_INFO.items():
|
||||
print(f"Updating {game} data...\n")
|
||||
if dal2.count(table_name=info['table']) == 0:
|
||||
if not dry_run:
|
||||
fill_game_data(game=game)
|
||||
else:
|
||||
dry_run_fill_game_data(game=game)
|
||||
else:
|
||||
most_recent = dal2.most_recent(table_name=info['table'])
|
||||
start_year = datetime.strptime(
|
||||
str(most_recent[0]), date_format,
|
||||
).year
|
||||
if not dry_run:
|
||||
fill_game_data(game=game, year=start_year)
|
||||
else:
|
||||
dry_run_fill_game_data(game=game, year=start_year)
|
||||
|
||||
|
||||
def get_table_counts(from_date: date | str | None) -> dict[str, int]:
|
||||
"""
|
||||
Loops over the tables in the database and gets a record count for each one.
|
||||
|
||||
:return: A dictionary of table names and record counts
|
||||
"""
|
||||
counts = {}
|
||||
for game, info in GAME_INFO.items():
|
||||
if isinstance(from_date, date):
|
||||
counts[game] = dal2.count(
|
||||
table_name=info['table'], from_date=from_date,
|
||||
)
|
||||
elif from_date == 'rule-change':
|
||||
rule_date = datetime.strptime(
|
||||
info['rule_change'], PROJECT_VARIABLES["LP_DATE_INSERT_FORMAT"],
|
||||
)
|
||||
counts[game] = dal2.count(
|
||||
table_name=info['table'], from_date=rule_date,
|
||||
)
|
||||
else:
|
||||
counts[game] = dal2.count(table_name=info['table'])
|
||||
return counts
|
||||
|
||||
|
||||
def get_prediction(
|
||||
game: str,
|
||||
window_size: int,
|
||||
test_case: bool = False,
|
||||
) -> tuple[dict, dict] | dict:
|
||||
# Get the list of drawings to make the prediction from
|
||||
from_date = GAME_INFO[game]['rule_change']
|
||||
|
||||
if test_case:
|
||||
if window_size == -1:
|
||||
drawings = analyze.load_dataframe_by_dates(
|
||||
game=game,
|
||||
start_date=from_date,
|
||||
)
|
||||
else:
|
||||
drawings = analyze.load_dataframe_most_recent(
|
||||
game=game,
|
||||
limit=window_size,
|
||||
)
|
||||
window_size = len(drawings) if window_size == -1 else window_size
|
||||
test_case = drawings.iloc[0]
|
||||
game_ball_out = 'powerball' if game == 'Powerball' else 'mega_ball'
|
||||
case_date = (test_case['draw_date'].
|
||||
strftime(PROJECT_VARIABLES['LP_DATE_INSERT_FORMAT']))
|
||||
case_main = [
|
||||
test_case['main_ball1'].tolist(),
|
||||
test_case['main_ball2'].tolist(),
|
||||
test_case['main_ball3'].tolist(),
|
||||
test_case['main_ball4'].tolist(),
|
||||
test_case['main_ball5'].tolist(),
|
||||
]
|
||||
case_game = test_case[game_ball_out].tolist()
|
||||
case_output = {
|
||||
'draw_date': case_date,
|
||||
'main_balls': case_main,
|
||||
'game_ball': case_game,
|
||||
}
|
||||
|
||||
predicted = analyze.make_prediction(
|
||||
data_frame=drawings.iloc[1:],
|
||||
window_size=(window_size - 2),
|
||||
)
|
||||
main_balls = [i.tolist() for i in predicted[0]]
|
||||
game_ball = int(predicted[1].astype(str))
|
||||
prediction_output = {'main_balls': main_balls, 'game_ball': game_ball}
|
||||
return case_output, prediction_output
|
||||
else:
|
||||
if window_size == -1:
|
||||
drawings = analyze.load_dataframe_by_dates(
|
||||
game=game,
|
||||
start_date=from_date,
|
||||
)
|
||||
else:
|
||||
drawings = analyze.load_dataframe_most_recent(
|
||||
game=game,
|
||||
limit=window_size,
|
||||
)
|
||||
window_size = len(drawings) if window_size == -1 else window_size
|
||||
predicted = analyze.make_prediction(
|
||||
data_frame=drawings,
|
||||
window_size=(window_size - 1),
|
||||
)
|
||||
main_balls = [int(i) for i in predicted[0].astype(str)]
|
||||
game_ball = int(predicted[1].astype(str))
|
||||
prediction_output = {'main_balls': main_balls, 'game_ball': game_ball}
|
||||
return prediction_output
|
||||
|
||||
|
||||
def check_game_name(game: str) -> bool:
|
||||
return game in GAME_INFO
|
||||
|
||||
|
||||
def main():
|
||||
print("Lottery Analyzer initializing...")
|
||||
# convert specific variables for use
|
||||
mm_start_year = int(project_variables["LP_MEGA_MILLIONS_INCEPTION"])
|
||||
pb_start_year = int(project_variables["LP_POWERBALL_INCEPTION"])
|
||||
end_year = datetime.today().year
|
||||
date_format = project_variables["LP_DATE_INSERT_FORMAT"]
|
||||
max_mm_main = int(project_variables["LP_MEGA_MILLIONS_MAX_MAIN_BALL"])
|
||||
max_megaball = int(project_variables["LP_MEGA_MILLIONS_MAX_MEGA_BALL"])
|
||||
max_pb_main = int(project_variables["LP_POWERBALL_MAX_MAIN_BALL"])
|
||||
max_powerball = int(project_variables["LP_POWERBALL_MAX_POWERBALL"])
|
||||
parser = argparse.ArgumentParser(description="A lottery prediction tool.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
help="Update database records",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"-y", "--year",
|
||||
type=int,
|
||||
help="Year to start updates with",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"-d", "--dryrun",
|
||||
action="store_true",
|
||||
help="Run the update process without actually making any "
|
||||
"database or API calls",
|
||||
)
|
||||
|
||||
# connect to the database
|
||||
print("Connecting to the database...")
|
||||
dal.connect()
|
||||
session = dal.Session()
|
||||
record_parser = subparsers.add_parser(
|
||||
"record-count",
|
||||
help="Get record counts by table in the database",
|
||||
)
|
||||
record_parser.add_argument(
|
||||
"-f", "--from-date",
|
||||
type=str,
|
||||
help="Date from which to base the count on (ex. 2026-01-01). "
|
||||
"The default value is rule-change for the date when game "
|
||||
"rules last changed",
|
||||
)
|
||||
|
||||
print("Collecting and processing missing Mega Millions draws...")
|
||||
# if there are no records in the MegaMillionsDraw table...
|
||||
if predictor.get_record_count('MegaMillionsDraw') == 0:
|
||||
# loop over all years and scrape the data for each
|
||||
for year in range(mm_start_year, end_year + 1):
|
||||
print(f"\tCollecting {year}...")
|
||||
scrape_data = scrape_game_history(game='mega-millions', year=year)
|
||||
# convert and insert the records
|
||||
for key, value in scrape_data.items():
|
||||
record = get_mega_millions_object(draw_date=key, draw_result=value)
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
pass
|
||||
# sleep between records and years to avoid querying the site too often
|
||||
sleep(60)
|
||||
ticket_parser = subparsers.add_parser(
|
||||
"generate-ticket",
|
||||
help="Generate a random ticket for the specified game",
|
||||
)
|
||||
ticket_parser.add_argument(
|
||||
"--game",
|
||||
type=str,
|
||||
choices=[game for game in GAME_INFO.keys()],
|
||||
required=True,
|
||||
help="The game to generate a random ticket for",
|
||||
)
|
||||
|
||||
# commit all the records to the database
|
||||
predict_parser = subparsers.add_parser(
|
||||
"predict",
|
||||
help="Predict the next drawing",
|
||||
)
|
||||
predict_parser.add_argument(
|
||||
"--game",
|
||||
type=str,
|
||||
choices=[game for game in GAME_INFO.keys()],
|
||||
required=True,
|
||||
help="The game to predict the next result for",
|
||||
)
|
||||
predict_parser.add_argument(
|
||||
"--window-size",
|
||||
type=int,
|
||||
default=10,
|
||||
help="The number of records to use in the prediction model, "
|
||||
"defaults to 10, use -1 for all records",
|
||||
)
|
||||
predict_parser.add_argument(
|
||||
"--test",
|
||||
action="store_true",
|
||||
help="Use the most recent drawing as a test subject to find "
|
||||
"the right window_size",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
match args.command:
|
||||
case "generate-ticket":
|
||||
if not check_game_name(args.game):
|
||||
print(f"{args.game} is not a valid game name.")
|
||||
sys.exit()
|
||||
main_balls, game_ball = ticket.generate_random_ticket(
|
||||
max_main=GAME_INFO[args.game]['max_main_ball'],
|
||||
max_game=GAME_INFO[args.game]['max_game_ball'],
|
||||
num_main=GAME_INFO[args.game]['main_count'],
|
||||
)
|
||||
print(
|
||||
f"Random {args.game} play: Main Balls: {main_balls}, "
|
||||
f"{GAME_INFO[args.game]['game_ball_name']}: {game_ball}",
|
||||
)
|
||||
case "record-count":
|
||||
# load the date format for the database
|
||||
date_format = PROJECT_VARIABLES["LP_DATE_INSERT_FORMAT"]
|
||||
if args.from_date:
|
||||
# if args.from_date is 'rule-change' use that date from
|
||||
# vars, otherwise use the date provided
|
||||
if args.from_date != 'rule-change':
|
||||
from_date = datetime.strptime(args.from_date, date_format)
|
||||
# get the table row counts from the date specified
|
||||
table_counts = get_table_counts(from_date=from_date)
|
||||
else:
|
||||
most_recent = session.query(func.max(MegaMillionsDraw.draw_date)).first()
|
||||
start_year = datetime.strptime(str(most_recent[0]), date_format).year
|
||||
# loop over the years needed and gather records
|
||||
for year in range(start_year, end_year + 1):
|
||||
print(f"\tCollecting {year}...")
|
||||
scrape_data = scrape_game_history(game='powerball', year=year)
|
||||
# convert and insert the records
|
||||
for key, value in scrape_data.items():
|
||||
record = get_powerball_object(draw_date=key, draw_result=value)
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
pass
|
||||
# sleep between records and years to avoid querying the site too often
|
||||
sleep(60)
|
||||
|
||||
print("Collecting and processing missing Powerball draws...")
|
||||
# if there are no records in the PowerBallDraw table...
|
||||
if len(session.query(PowerballDraw).all()) == 0:
|
||||
# loop over all years and scrape the data for each
|
||||
for year in range(pb_start_year, end_year + 1):
|
||||
print(f"\tCollecting {year}...")
|
||||
scrape_data = scrape_game_history(game='powerball', year=year)
|
||||
# convert and insert the records
|
||||
for key, value in scrape_data.items():
|
||||
record = get_powerball_object(draw_date=key, draw_result=value)
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
pass
|
||||
# sleep between records and years to avoid querying the site too often
|
||||
sleep(60)
|
||||
|
||||
# commit all the records to the database
|
||||
session.commit()
|
||||
table_counts = get_table_counts(from_date=args.from_date)
|
||||
else:
|
||||
most_recent = session.query(func.max(PowerballDraw.draw_date)).first()
|
||||
start_year = datetime.strptime(most_recent[0], str(date_format)).year
|
||||
# loop over the years needed and gather records
|
||||
for year in range(start_year, end_year + 1):
|
||||
print(f"\tCollecting {year}...")
|
||||
scrape_data = scrape_game_history(game='powerball', year=year)
|
||||
# convert and insert the records
|
||||
for key, value in scrape_data.items():
|
||||
record = get_powerball_object(draw_date=key, draw_result=value)
|
||||
try:
|
||||
session.add(record)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
pass
|
||||
# sleep between records and years to avoid querying the site too often
|
||||
sleep(60)
|
||||
|
||||
|
||||
|
||||
mm_count = session.query(MegaMillionsDraw).count()
|
||||
pb_count = session.query(PowerballDraw).count()
|
||||
|
||||
print(f"There are {mm_count} Mega Millions draw records and {pb_count} Powerball records")
|
||||
table_counts = get_table_counts(from_date=None)
|
||||
for table, count in table_counts.items():
|
||||
print(f"{table}:\t{count}")
|
||||
case "update":
|
||||
update_games(dry_run=args.dryrun)
|
||||
case "predict":
|
||||
if not check_game_name(args.game):
|
||||
print(f"{args.game} is not a valid game name.")
|
||||
sys.exit()
|
||||
if args.test:
|
||||
recent, prediction = get_prediction(
|
||||
game=args.game,
|
||||
window_size=args.window_size,
|
||||
test_case=args.test,
|
||||
)
|
||||
print(
|
||||
f"{args.game} prediction: Main Balls: "
|
||||
f"{prediction['main_balls']}, "
|
||||
f"{GAME_INFO[args.game]['game_ball_name']}: "
|
||||
f"{prediction['game_ball']}",
|
||||
)
|
||||
print(
|
||||
f"{recent['draw_date']} drawing : "
|
||||
f"Main Balls: {recent['main_balls']}, "
|
||||
f"{GAME_INFO[args.game]['game_ball_name']}: "
|
||||
f"{recent['game_ball']}",
|
||||
)
|
||||
else:
|
||||
prediction = get_prediction(
|
||||
game=args.game,
|
||||
window_size=args.window_size,
|
||||
test_case=args.test,
|
||||
)
|
||||
print(
|
||||
f"{args.game} prediction: Main Balls: "
|
||||
f"{prediction['main_balls']}, "
|
||||
f"{GAME_INFO[args.game]['game_ball_name']}: "
|
||||
f"{prediction['game_ball']}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Regular → Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,8 @@ dependencies = [
|
||||
"python-dotenv>=1.2.1",
|
||||
"scikit-learn>=1.8.0",
|
||||
"sqlalchemy>=2.0.46",
|
||||
"sqlalchemy-utils>=0.42.1",
|
||||
"tqdm>=4.67.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from httpx import ConnectError
|
||||
|
||||
from conftest import MegaMillionsDraw
|
||||
from httpx import ConnectError
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from util.drawing import get_mega_millions_object
|
||||
from util.scrape import scrape_game_history
|
||||
|
||||
@@ -12,13 +12,18 @@ from util.scrape import scrape_game_history
|
||||
def test_create_mega_millions_draw(db_session) -> None:
|
||||
"""add a record"""
|
||||
date = datetime(2010, 2, 2).date() # 6 7 26 27 49 9 4x
|
||||
new_record = MegaMillionsDraw(draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26, main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4)
|
||||
new_record = MegaMillionsDraw(
|
||||
draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26,
|
||||
main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4,
|
||||
)
|
||||
db_session.add(new_record)
|
||||
db_session.commit()
|
||||
|
||||
# verify the record exists
|
||||
record = db_session.query(MegaMillionsDraw).filter_by(draw_date=date).first()
|
||||
record = db_session.query(MegaMillionsDraw).filter_by(
|
||||
draw_date=date,
|
||||
).first()
|
||||
assert record is not None
|
||||
assert record == new_record
|
||||
|
||||
@@ -31,15 +36,20 @@ def test_mega_millions_draw_isolation(db_session) -> None:
|
||||
|
||||
def test_mega_millions_draw_by_date_blank(db_session) -> None:
|
||||
"""query for all mega millions records with a blank date"""
|
||||
blank_results = db_session.query(MegaMillionsDraw).filter_by(draw_date='').all()
|
||||
blank_results = db_session.query(MegaMillionsDraw).filter_by(
|
||||
draw_date='',
|
||||
).all()
|
||||
assert len(blank_results) == 0
|
||||
|
||||
|
||||
def test_mega_millions_draw_integrity(db_session) -> None:
|
||||
"""ensure that duplicate records cannot be inserted"""
|
||||
date = datetime(2010, 2, 2).date() # 6 7 26 27 49 9 4x
|
||||
new_record = MegaMillionsDraw(draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26, main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4)
|
||||
new_record = MegaMillionsDraw(
|
||||
draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26,
|
||||
main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4,
|
||||
)
|
||||
|
||||
try:
|
||||
# insert a new record
|
||||
@@ -56,15 +66,20 @@ def test_mega_millions_draw_integrity(db_session) -> None:
|
||||
def test_mega_millions_draw_eq_repr_and_str(db_session) -> None:
|
||||
"""test the __eq__, __repr__ and __str__ methods of MegaMillionsDraw"""
|
||||
date = datetime(2010, 2, 2).date()
|
||||
new_record = MegaMillionsDraw(draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26, main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4)
|
||||
new_record = MegaMillionsDraw(
|
||||
draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26,
|
||||
main_ball4=27,
|
||||
main_ball5=49, mega_ball=9, megaplier=4,
|
||||
)
|
||||
|
||||
# insert a new record
|
||||
db_session.add(new_record)
|
||||
db_session.commit()
|
||||
|
||||
# query the record
|
||||
record = db_session.query(MegaMillionsDraw).filter_by(draw_date=date).first()
|
||||
record = db_session.query(MegaMillionsDraw).filter_by(
|
||||
draw_date=date,
|
||||
).first()
|
||||
|
||||
# compare the str and repr values of each using __eq__
|
||||
assert str(record) == str(new_record)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from httpx import ConnectError
|
||||
|
||||
from conftest import PowerballDraw
|
||||
from httpx import ConnectError
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from util.drawing import get_powerball_object
|
||||
from util.scrape import scrape_game_history
|
||||
|
||||
@@ -12,8 +12,11 @@ from util.scrape import scrape_game_history
|
||||
def test_create_powerball_draw(db_session) -> None:
|
||||
"""add a record"""
|
||||
date = datetime(2010, 2, 3).date()
|
||||
new_record = PowerballDraw(draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36, main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2)
|
||||
new_record = PowerballDraw(
|
||||
draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36,
|
||||
main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2,
|
||||
)
|
||||
db_session.add(new_record)
|
||||
db_session.commit()
|
||||
|
||||
@@ -31,15 +34,20 @@ def test_powerball_draw_isolation(db_session) -> None:
|
||||
|
||||
def test_powerball_draw_by_date_blank(db_session) -> None:
|
||||
"""query for all powerball records with a blank date"""
|
||||
blank_results = db_session.query(PowerballDraw).filter_by(draw_date='').all()
|
||||
blank_results = db_session.query(PowerballDraw).filter_by(
|
||||
draw_date='',
|
||||
).all()
|
||||
assert len(blank_results) == 0
|
||||
|
||||
|
||||
def test_powerball_draw_integrity(db_session) -> None:
|
||||
"""ensure that duplicate records cannot be inserted"""
|
||||
date = datetime(2010, 2, 3).date()
|
||||
new_record = PowerballDraw(draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36, main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2)
|
||||
new_record = PowerballDraw(
|
||||
draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36,
|
||||
main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2,
|
||||
)
|
||||
|
||||
try:
|
||||
# insert a new record
|
||||
@@ -56,8 +64,11 @@ def test_powerball_draw_integrity(db_session) -> None:
|
||||
def test_powerball_draw_eq_repr_and_str(db_session) -> None:
|
||||
"""test the __eq__, __repr__ and __str__ methods of PowerballDraw"""
|
||||
date = datetime(2010, 2, 3).date()
|
||||
new_record = PowerballDraw(draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36, main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2)
|
||||
new_record = PowerballDraw(
|
||||
draw_date=date, main_ball1=17, main_ball2=22, main_ball3=36,
|
||||
main_ball4=37,
|
||||
main_ball5=52, powerball=24, power_play=2,
|
||||
)
|
||||
|
||||
# insert a new record
|
||||
db_session.add(new_record)
|
||||
|
||||
Regular → Executable
+120
-36
@@ -1,19 +1,68 @@
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import lottery_predictor.analyze as analyze
|
||||
|
||||
|
||||
def test_load_dataframe() -> None:
|
||||
def test_load_dataframe_by_date() -> None:
|
||||
pb_start = datetime(2015, 10, 4)
|
||||
mm_start = datetime(2025, 4, 5)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
assert int(analyze.load_dataframe(
|
||||
table_name='PowerballDraw', start_date=pb_start, end_date=date_end).count()['draw_date']) == 1324
|
||||
assert int(analyze.load_dataframe(
|
||||
table_name='MegaMillionsDraw', start_date=mm_start, end_date=date_end).count()['draw_date']) == 96
|
||||
assert int(
|
||||
analyze.load_dataframe_by_dates(
|
||||
game='Powerball', start_date=pb_start, end_date=date_end,
|
||||
).count()['draw_date'],
|
||||
) >= 1324
|
||||
assert int(
|
||||
analyze.load_dataframe_by_dates(
|
||||
game='MegaMillions', start_date=mm_start, end_date=date_end,
|
||||
).count()['draw_date'],
|
||||
) >= 96
|
||||
with pytest.raises(KeyError):
|
||||
analyze.load_dataframe_by_dates(game='SomeNonExistentGameName')
|
||||
|
||||
|
||||
def test_load_dataframe_most_recent() -> None:
|
||||
assert len(analyze.load_dataframe_most_recent(game='Powerball')) == 10
|
||||
assert len(analyze.load_dataframe_most_recent(game='MegaMillions')) == 10
|
||||
assert len(
|
||||
analyze.load_dataframe_most_recent(game='Powerball', limit=105),
|
||||
) == 105
|
||||
assert len(
|
||||
analyze.load_dataframe_most_recent(game='MegaMillions', limit=45),
|
||||
) == 45
|
||||
with pytest.raises(ValueError):
|
||||
analyze.load_dataframe(table_name='SomNonExistentTableName')
|
||||
analyze.load_dataframe_most_recent(game='Powerball', limit=-1)
|
||||
analyze.load_dataframe_most_recent(game='DoesntExist')
|
||||
|
||||
|
||||
def test_prepare_split_data() -> None:
|
||||
mm_start = datetime(2025, 4, 5)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
mega = analyze.load_dataframe_by_dates(
|
||||
game='MegaMillions', start_date=mm_start, end_date=date_end,
|
||||
)
|
||||
|
||||
x, y1, y2 = analyze.prepare_split_data(
|
||||
data=mega.values, window_size=len(mega) - 1,
|
||||
)
|
||||
assert isinstance(x, np.ndarray)
|
||||
assert isinstance(y1, np.ndarray)
|
||||
assert isinstance(y2, np.ndarray)
|
||||
|
||||
|
||||
def test_make_prediction() -> None:
|
||||
pb_start = datetime(2015, 10, 4)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
power = analyze.load_dataframe_by_dates(
|
||||
game='Powerball', start_date=pb_start, end_date=date_end,
|
||||
)
|
||||
|
||||
main, game = analyze.make_prediction(data_frame=power, window_size=10)
|
||||
assert isinstance(main, np.ndarray)
|
||||
assert isinstance(game, np.int64)
|
||||
|
||||
|
||||
def test_least_and_most_common_number() -> None:
|
||||
@@ -21,35 +70,56 @@ def test_least_and_most_common_number() -> None:
|
||||
mm_start = datetime(2025, 4, 5)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
|
||||
pb_df = analyze.load_dataframe(
|
||||
table_name='PowerballDraw', start_date=pb_start, end_date=date_end)
|
||||
mm_df = analyze.load_dataframe(
|
||||
table_name='MegaMillionsDraw', start_date=mm_start, end_date=date_end)
|
||||
pb_df = analyze.load_dataframe_by_dates(
|
||||
game='Powerball', start_date=pb_start, end_date=date_end,
|
||||
)
|
||||
mm_df = analyze.load_dataframe_by_dates(
|
||||
game='MegaMillions', start_date=mm_start, end_date=date_end,
|
||||
)
|
||||
|
||||
assert analyze.get_most_common_number(pb_df, top=5) == [61, 21, 28, 23, 33]
|
||||
assert analyze.get_least_common_number(pb_df, bottom=5) == [13, 49, 46, 26, 34]
|
||||
assert analyze.get_most_common_number(mm_df, top=5) == [42, 40, 18, 10, 49]
|
||||
assert analyze.get_least_common_number(mm_df, bottom=5) == [35, 51, 3, 1, 20]
|
||||
assert analyze.get_most_common_number(pb_df, top=5) == [61, 21, 23, 28, 33]
|
||||
assert analyze.get_least_common_number(pb_df, bottom=5) == [
|
||||
13, 49, 26, 46, 34,
|
||||
]
|
||||
assert analyze.get_most_common_number(mm_df, top=5) == [42, 18, 40, 49, 10]
|
||||
assert analyze.get_least_common_number(mm_df, bottom=5) == [
|
||||
35, 51, 61, 1, 20,
|
||||
]
|
||||
|
||||
assert analyze.get_most_common_number(pb_df, columns=['powerball'], top=1) == [4]
|
||||
assert analyze.get_least_common_number(pb_df, columns=['powerball'], bottom=1) == [16]
|
||||
assert analyze.get_most_common_number(mm_df, columns=['mega_ball'], top=1) == [1]
|
||||
assert analyze.get_least_common_number(mm_df, columns=['mega_ball'], bottom=1) == [3]
|
||||
assert analyze.get_most_common_number(
|
||||
pb_df, columns=['powerball'], top=1,
|
||||
) == [4]
|
||||
assert analyze.get_least_common_number(
|
||||
pb_df, columns=['powerball'], bottom=1,
|
||||
) == [16]
|
||||
assert analyze.get_most_common_number(
|
||||
mm_df, columns=['mega_ball'], top=1,
|
||||
) == [24]
|
||||
assert analyze.get_least_common_number(
|
||||
mm_df, columns=['mega_ball'], bottom=1,
|
||||
) == [20]
|
||||
|
||||
|
||||
def test_calculate_probabilities() -> None:
|
||||
"""tests build_binary_matrix, build_next_targets and calculate_number_probability"""
|
||||
"""tests build_binary_matrix, build_next_targets and
|
||||
calculate_number_probability"""
|
||||
pb_start = datetime(2015, 10, 4)
|
||||
mm_start = datetime(2025, 4, 5)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
|
||||
pb_df = analyze.load_dataframe(
|
||||
table_name='PowerballDraw', start_date=pb_start, end_date=date_end)
|
||||
pb_probabilities = analyze.calculate_probabilities(data_frame=pb_df, max_number=69)
|
||||
pb_df = analyze.load_dataframe_by_dates(
|
||||
game='Powerball', start_date=pb_start, end_date=date_end,
|
||||
)
|
||||
pb_probabilities = analyze.calculate_probabilities(
|
||||
data_frame=pb_df, max_number=69,
|
||||
)
|
||||
|
||||
mm_df = analyze.load_dataframe(
|
||||
table_name='MegaMillionsDraw', start_date=mm_start, end_date=date_end)
|
||||
mm_probabilities = analyze.calculate_probabilities(data_frame=mm_df, max_number=70)
|
||||
mm_df = analyze.load_dataframe_by_dates(
|
||||
game='MegaMillions', start_date=mm_start, end_date=date_end,
|
||||
)
|
||||
mm_probabilities = analyze.calculate_probabilities(
|
||||
data_frame=mm_df, max_number=70,
|
||||
)
|
||||
|
||||
assert pb_probabilities[1] == 0.3142857142857143
|
||||
assert mm_probabilities[1] == 0.04225352112676056
|
||||
@@ -61,21 +131,35 @@ def test_hot_cold_numbers() -> None:
|
||||
mm_start = datetime(2025, 4, 5)
|
||||
date_end = datetime(2026, 3, 8)
|
||||
|
||||
pb_df = analyze.load_dataframe(
|
||||
table_name='PowerballDraw', start_date=pb_start, end_date=date_end)
|
||||
pb_df = analyze.load_dataframe_by_dates(
|
||||
game='Powerball', start_date=pb_start, end_date=date_end,
|
||||
)
|
||||
pb_probs = analyze.calculate_probabilities(data_frame=pb_df, max_number=69)
|
||||
mm_df = analyze.load_dataframe(
|
||||
table_name='MegaMillionsDraw', start_date=mm_start, end_date=date_end)
|
||||
mm_df = analyze.load_dataframe_by_dates(
|
||||
game='MegaMillions', start_date=mm_start, end_date=date_end,
|
||||
)
|
||||
mm_probs = analyze.calculate_probabilities(data_frame=mm_df, max_number=70)
|
||||
|
||||
assert (analyze.get_hot_numbers(probabilities=pb_probs) ==
|
||||
[(13, 1.0), (61, 0.7), (21, 0.6714285714285715), (23, 0.6428571428571428), (28, 0.6428571428571428)])
|
||||
[
|
||||
(13, 1.0), (61, 0.7), (21, 0.6714285714285715),
|
||||
(23, 0.6428571428571428), (28, 0.6428571428571428),
|
||||
])
|
||||
assert (analyze.get_cold_numbers(probabilities=pb_probs) ==
|
||||
[(49, 0.10000000000000009), (26, 0.11428571428571432), (46, 0.11428571428571432),
|
||||
(34, 0.17142857142857149), (65, 0.18571428571428572)])
|
||||
[
|
||||
(49, 0.10000000000000009), (26, 0.11428571428571432),
|
||||
(46, 0.11428571428571432),
|
||||
(34, 0.17142857142857149), (65, 0.18571428571428572),
|
||||
])
|
||||
assert (analyze.get_hot_numbers(probabilities=mm_probs) ==
|
||||
[(42, 0.19718309859154928), (18, 0.18309859154929578), (40, 0.18309859154929578),
|
||||
(10, 0.16901408450704225), (49, 0.16901408450704225)])
|
||||
[
|
||||
(42, 0.19718309859154928), (18, 0.18309859154929578),
|
||||
(40, 0.18309859154929578),
|
||||
(10, 0.16901408450704225), (49, 0.16901408450704225),
|
||||
])
|
||||
assert (analyze.get_cold_numbers(probabilities=mm_probs) ==
|
||||
[(35, 0.028169014084507043), (51, 0.028169014084507043), (1, 0.04225352112676056),
|
||||
(3, 0.04225352112676056), (20, 0.04225352112676056)])
|
||||
[
|
||||
(35, 0.028169014084507043), (51, 0.028169014084507043),
|
||||
(1, 0.04225352112676056),
|
||||
(3, 0.04225352112676056), (20, 0.04225352112676056),
|
||||
])
|
||||
@@ -1,25 +1,30 @@
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
import lottery_predictor.collect as collect
|
||||
|
||||
|
||||
def test_get_record_count() -> None:
|
||||
assert isinstance(collect.get_record_count(table_name='PowerballDraw'), int)
|
||||
assert isinstance(collect.get_record_count(table_name='MegaMillionsDraw'), int)
|
||||
with pytest.raises(ValueError):
|
||||
collect.get_record_count(table_name='SomeNonExistentTable')
|
||||
assert isinstance(collect.get_record_count(game='Powerball'), int)
|
||||
assert isinstance(collect.get_record_count(game='MegaMillions'), int)
|
||||
with pytest.raises(KeyError):
|
||||
collect.get_record_count(game='SomeNonExistentTable')
|
||||
|
||||
|
||||
def test_get_recent_draw_date() -> None:
|
||||
assert isinstance(collect.get_recent_draw_date(table_name='PowerballDraw'), datetime.date)
|
||||
assert isinstance(collect.get_recent_draw_date(table_name='MegaMillionsDraw'), datetime.date)
|
||||
with pytest.raises(ValueError):
|
||||
collect.get_recent_draw_date(table_name='SomeNonExistentTable')
|
||||
assert isinstance(
|
||||
collect.get_recent_draw_date(game='Powerball'), datetime.date,
|
||||
)
|
||||
assert isinstance(
|
||||
collect.get_recent_draw_date(game='MegaMillions'), datetime.date,
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
collect.get_recent_draw_date(game='SomeNonExistentTable')
|
||||
|
||||
|
||||
def test_get_missing_records() -> None:
|
||||
assert isinstance(collect.get_missing_records(table_name='PowerballDraw'), int)
|
||||
assert isinstance(collect.get_missing_records(table_name='MegaMillionsDraw'), int)
|
||||
with pytest.raises(ValueError):
|
||||
collect.get_missing_records(table_name='SomeNonExistentTable')
|
||||
assert isinstance(collect.get_missing_records(game='Powerball'), int)
|
||||
assert isinstance(collect.get_missing_records(game='MegaMillions'), int)
|
||||
with pytest.raises(KeyError):
|
||||
collect.get_missing_records(game='SomeNonExistentTable')
|
||||
@@ -4,7 +4,11 @@ import lottery_predictor.draw as draw
|
||||
|
||||
|
||||
def test_generate_random_ticket() -> None:
|
||||
assert isinstance(draw.generate_random_ticket(max_main=69, max_game=26), tuple)
|
||||
assert isinstance(draw.generate_random_ticket(max_main=70, max_game=24), tuple)
|
||||
assert isinstance(
|
||||
draw.generate_random_ticket(max_main=69, max_game=26), tuple,
|
||||
)
|
||||
assert isinstance(
|
||||
draw.generate_random_ticket(max_main=70, max_game=24), tuple,
|
||||
)
|
||||
with pytest.raises(TypeError):
|
||||
draw.generate_random_ticket()
|
||||
Regular → Executable
+103
-46
@@ -1,21 +1,15 @@
|
||||
"""Tests for the `util` module."""
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from httpx import ConnectError
|
||||
|
||||
import pytest
|
||||
import util.convert as uc
|
||||
import util.drawing as ud
|
||||
import util.environment as ue
|
||||
import util.scrape as sc
|
||||
import util.odds as uo
|
||||
from data.database import PowerballDraw, MegaMillionsDraw
|
||||
|
||||
|
||||
def test_check_table_name() -> None:
|
||||
assert isinstance(ud.check_table_name('PowerballDraw'), dict)
|
||||
assert isinstance(ud.check_table_name('MegaMillionsDraw'), dict)
|
||||
assert ud.check_table_name('SomeNonExistentTableName') is None
|
||||
import util.scrape as sc
|
||||
from data.database import MegaMillionsDraw, PowerballDraw
|
||||
|
||||
|
||||
def test_convert_string_to_integer() -> None:
|
||||
@@ -37,7 +31,8 @@ def test_get_previous_draw_date_none() -> None:
|
||||
|
||||
|
||||
def test_get_next_draw_date_minimum() -> None:
|
||||
"""Test the next draw date returned for each game and when an invalid name is presented (None)"""
|
||||
"""Test the next draw date returned for each game and when an invalid
|
||||
name is presented (None)"""
|
||||
assert ud.get_next_draw_date(game="mega-millions")
|
||||
assert ud.get_next_draw_date(game="powerball")
|
||||
assert not ud.get_next_draw_date(game="something")
|
||||
@@ -46,7 +41,8 @@ def test_get_next_draw_date_minimum() -> None:
|
||||
|
||||
|
||||
def test_get_previous_draw_date_minimum() -> None:
|
||||
"""Test the next draw date returned for each game and when an invalid name is presented (None)"""
|
||||
"""Test the next draw date returned for each game and when an invalid
|
||||
name is presented (None)"""
|
||||
assert ud.get_previous_draw_date(game="mega-millions")
|
||||
assert ud.get_previous_draw_date(game="powerball")
|
||||
assert not ud.get_previous_draw_date(game="something")
|
||||
@@ -56,39 +52,84 @@ def test_get_previous_draw_date_minimum() -> None:
|
||||
|
||||
def test_get_next_draw_date_specified_from_date() -> None:
|
||||
"""Test drawing dates returned for each possible day of the week."""
|
||||
assert ud.get_next_draw_date(game="mega-millions", from_date='02/15/2026') == '02/17/2026'
|
||||
assert ud.get_next_draw_date(game="mega-millions", from_date='02/16/2026') == '02/17/2026'
|
||||
assert ud.get_next_draw_date(game="mega-millions", from_date='02/17/2026') == '02/17/2026'
|
||||
assert ud.get_next_draw_date(game="mega-millions", from_date='02/18/2026') == '02/20/2026'
|
||||
assert ud.get_next_draw_date(game="mega-millions", from_date='02/19/2026') == '02/20/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game="mega-millions", from_date='02/15/2026',
|
||||
) == '02/17/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game="mega-millions", from_date='02/16/2026',
|
||||
) == '02/17/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game="mega-millions", from_date='02/17/2026',
|
||||
) == '02/17/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game="mega-millions", from_date='02/18/2026',
|
||||
) == '02/20/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game="mega-millions", from_date='02/19/2026',
|
||||
) == '02/20/2026'
|
||||
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/15/2026') == '02/16/2026'
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/16/2026') == '02/16/2026'
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/17/2026') == '02/18/2026'
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/18/2026') == '02/18/2026'
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/19/2026') == '02/21/2026'
|
||||
assert ud.get_next_draw_date(game='powerball', from_date='02/20/2026') == '02/21/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/15/2026',
|
||||
) == '02/16/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/16/2026',
|
||||
) == '02/16/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/17/2026',
|
||||
) == '02/18/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/18/2026',
|
||||
) == '02/18/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/19/2026',
|
||||
) == '02/21/2026'
|
||||
assert ud.get_next_draw_date(
|
||||
game='powerball', from_date='02/20/2026',
|
||||
) == '02/21/2026'
|
||||
|
||||
|
||||
def test_get_previous_draw_date_specified_from_date() -> None:
|
||||
"""Test drawing dates returned for each possible day of the week."""
|
||||
assert ud.get_previous_draw_date(game="mega-millions", from_date='02/15/2026') == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(game="mega-millions", from_date='02/16/2026') == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(game="mega-millions", from_date='02/17/2026') == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(game="mega-millions", from_date='02/18/2026') == '02/17/2026'
|
||||
assert ud.get_previous_draw_date(game="mega-millions", from_date='02/19/2026') == '02/17/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game="mega-millions", from_date='02/15/2026',
|
||||
) == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game="mega-millions", from_date='02/16/2026',
|
||||
) == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game="mega-millions", from_date='02/17/2026',
|
||||
) == '02/13/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game="mega-millions", from_date='02/18/2026',
|
||||
) == '02/17/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game="mega-millions", from_date='02/19/2026',
|
||||
) == '02/17/2026'
|
||||
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/15/2026') == '02/14/2026'
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/16/2026') == '02/14/2026'
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/17/2026') == '02/16/2026'
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/18/2026') == '02/16/2026'
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/19/2026') == '02/18/2026'
|
||||
assert ud.get_previous_draw_date(game='powerball', from_date='02/20/2026') == '02/18/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/15/2026',
|
||||
) == '02/14/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/16/2026',
|
||||
) == '02/14/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/17/2026',
|
||||
) == '02/16/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/18/2026',
|
||||
) == '02/16/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/19/2026',
|
||||
) == '02/18/2026'
|
||||
assert ud.get_previous_draw_date(
|
||||
game='powerball', from_date='02/20/2026',
|
||||
) == '02/18/2026'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("game", ['powerball', 'mega-millions'])
|
||||
def test_scrape_game_history(game) -> None:
|
||||
"""Test scraping the powerball result page for 1996, 2020, and current year."""
|
||||
"""Test scraping the powerball result page for 1996, 2020, and current
|
||||
year."""
|
||||
try:
|
||||
scrape_data = sc.scrape_game_history(game, year=1996)
|
||||
assert len(scrape_data) in [17, 104]
|
||||
@@ -112,31 +153,47 @@ def test_calculate_odds() -> None:
|
||||
|
||||
def test_get_odds_of_winning() -> None:
|
||||
assert uo.get_odds_of_winning(
|
||||
main_ball_draw_pool=49, main_ball_draw_count=5, game_ball_draw_pool=18, game_ball_draw_count=1
|
||||
main_ball_draw_pool=49, main_ball_draw_count=5, game_ball_draw_pool=18,
|
||||
game_ball_draw_count=1,
|
||||
) == 'The odds of winning are 1 in 34,323,912'
|
||||
with pytest.raises(TypeError):
|
||||
uo.get_odds_of_winning()
|
||||
with pytest.raises(ValueError):
|
||||
uo.get_odds_of_winning(
|
||||
main_ball_draw_pool=49, main_ball_draw_count= 50, game_ball_draw_pool=18, game_ball_draw_count=1)
|
||||
main_ball_draw_pool=49, main_ball_draw_count=50,
|
||||
game_ball_draw_pool=18, game_ball_draw_count=1,
|
||||
)
|
||||
assert uo.get_odds_of_winning(
|
||||
main_ball_draw_pool=69, main_ball_draw_count=5, game_ball_draw_pool=26, game_ball_draw_count=1
|
||||
main_ball_draw_pool=69, main_ball_draw_count=5, game_ball_draw_pool=26,
|
||||
game_ball_draw_count=1,
|
||||
) == 'The odds of winning are 1 in 292,201,338'
|
||||
|
||||
|
||||
def test_get_powerball_object() -> None:
|
||||
data_value = ud.get_powerball_object(draw_date='2026-01-03', draw_result={'main_balls': [18, 21, 40, 53, 60],
|
||||
'powerball': 23, 'multiplier': 3})
|
||||
test_record = PowerballDraw(draw_date=datetime(2026, 1, 3).date(), main_ball1=18, main_ball2=21,
|
||||
main_ball3=40, main_ball4=53, main_ball5=60, powerball=23, power_play=3)
|
||||
data_value = ud.get_powerball_object(
|
||||
draw_date='2026-01-03', draw_result={
|
||||
'main_balls': [18, 21, 40, 53, 60],
|
||||
'powerball': 23, 'multiplier': 3,
|
||||
},
|
||||
)
|
||||
test_record = PowerballDraw(
|
||||
draw_date=datetime(2026, 1, 3).date(), main_ball1=18, main_ball2=21,
|
||||
main_ball3=40, main_ball4=53, main_ball5=60, powerball=23, power_play=3,
|
||||
)
|
||||
assert data_value == test_record
|
||||
|
||||
|
||||
def test_get_mega_millions_object() -> None:
|
||||
data_value = ud.get_mega_millions_object(draw_date='2010-02-02', draw_result={'main_balls': [6, 7, 26, 27, 49],
|
||||
'mega-ball': 9, 'multiplier': 4})
|
||||
test_record = MegaMillionsDraw(draw_date=datetime(2010, 2, 2).date(), main_ball1=6, main_ball2=7,
|
||||
main_ball3=26, main_ball4=27, main_ball5=49, mega_ball=9, megaplier=4)
|
||||
data_value = ud.get_mega_millions_object(
|
||||
draw_date='2010-02-02', draw_result={
|
||||
'main_balls': [6, 7, 26, 27, 49],
|
||||
'mega-ball': 9, 'multiplier': 4,
|
||||
},
|
||||
)
|
||||
test_record = MegaMillionsDraw(
|
||||
draw_date=datetime(2010, 2, 2).date(), main_ball1=6, main_ball2=7,
|
||||
main_ball3=26, main_ball4=27, main_ball5=49, mega_ball=9, megaplier=4,
|
||||
)
|
||||
assert data_value == test_record
|
||||
|
||||
|
||||
@@ -144,4 +201,4 @@ def test_load_environment_variables() -> None:
|
||||
"""test loading environment variables for script use"""
|
||||
env_vars = ue.load_environment_variables()
|
||||
assert type(env_vars) == dict
|
||||
assert len(env_vars) == 24
|
||||
assert isinstance(len(env_vars), int)
|
||||
+3
-4
@@ -1,6 +1,3 @@
|
||||
import re
|
||||
|
||||
|
||||
class InvalidStringException(Exception):
|
||||
"""Raised when a string cannot be converted to an integer."""
|
||||
pass
|
||||
@@ -25,4 +22,6 @@ def convert_string_to_integer(data: str) -> int | Exception:
|
||||
else:
|
||||
return int(data)
|
||||
except ValueError as exc:
|
||||
raise InvalidStringException("The string could not be converted to an integer.") from exc
|
||||
raise InvalidStringException(
|
||||
"The string could not be converted to an integer.",
|
||||
) from exc
|
||||
+46
-22
@@ -4,16 +4,17 @@
|
||||
Utility functions for drawing data processing
|
||||
"""
|
||||
import os
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from data.database import PowerballDraw, MegaMillionsDraw
|
||||
from data.database import MegaMillionsDraw, PowerballDraw, Table
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def check_table_name(table_name: str) -> dict | None:
|
||||
def check_table_name2(table_name: Table) -> dict | None:
|
||||
"""
|
||||
:param table_name: table name
|
||||
:return: dict of game info or None
|
||||
@@ -38,13 +39,17 @@ def check_table_name(table_name: str) -> dict | None:
|
||||
return game_dict[table_name] if table_name in game_dict.keys() else None
|
||||
|
||||
|
||||
def get_next_draw_date(game: str | None, from_date: str | None = None) -> str | None:
|
||||
def get_next_draw_date(
|
||||
game: str | None, from_date: str | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Gets the next drawing date from the current or specified date for the given game
|
||||
Gets the next drawing date from the current or specified date for the
|
||||
given game
|
||||
If the current date is the next drawing date, the current date is returned
|
||||
|
||||
:param game: megamillions or powerball
|
||||
:param from_date: date to get next drawing date from, if omitted the current date is used
|
||||
:param from_date: date to get next drawing date from, if omitted the
|
||||
current date is used
|
||||
:return: next drawing date
|
||||
|
||||
Examples:
|
||||
@@ -55,7 +60,7 @@ def get_next_draw_date(game: str | None, from_date: str | None = None) -> str |
|
||||
"""
|
||||
target_days = {
|
||||
"powerball": [0, 2, 5],
|
||||
"mega-millions": [1, 4]
|
||||
"mega-millions": [1, 4],
|
||||
}
|
||||
|
||||
return_date = None
|
||||
@@ -77,13 +82,18 @@ def get_next_draw_date(game: str | None, from_date: str | None = None) -> str |
|
||||
return return_date.strftime(return_date_format)
|
||||
|
||||
|
||||
def get_previous_draw_date(game: str | None, from_date: str | None = None) -> str | None:
|
||||
def get_previous_draw_date(
|
||||
game: str | None, from_date: str | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Gets the previous drawing date from the current or specified date for the given game
|
||||
If the current date is a drawing date, the next previous date will be returned
|
||||
Gets the previous drawing date from the current or specified date for the
|
||||
given game
|
||||
If the current date is a drawing date, the next previous date will be
|
||||
returned
|
||||
|
||||
:param game: megamillions or powerball
|
||||
:param from_date: date to get previous drawing date from, if omitted the current date is used
|
||||
:param from_date: date to get previous drawing date from, if omitted the
|
||||
current date is used
|
||||
:return: previous drawing date
|
||||
|
||||
Examples:
|
||||
@@ -94,7 +104,7 @@ def get_previous_draw_date(game: str | None, from_date: str | None = None) -> st
|
||||
"""
|
||||
target_days = {
|
||||
"powerball": [0, 2, 5],
|
||||
"mega-millions": [1, 4]
|
||||
"mega-millions": [1, 4],
|
||||
}
|
||||
|
||||
return_date = None
|
||||
@@ -110,7 +120,8 @@ def get_previous_draw_date(game: str | None, from_date: str | None = None) -> st
|
||||
else:
|
||||
return_date = datetime.strptime(from_date, return_date_format)
|
||||
|
||||
# to ensure the current date is not returned if it is a draw date, subtract a day first
|
||||
# to ensure the current date is not returned if it is a draw date,
|
||||
# subtract a day first
|
||||
return_date -= timedelta(days=1)
|
||||
|
||||
while return_date.weekday() not in target_days[game]:
|
||||
@@ -126,32 +137,45 @@ def get_powerball_object(draw_date: str, draw_result: dict) -> PowerballDraw:
|
||||
:return: PowerballDraw object
|
||||
|
||||
Examples:
|
||||
>>> get_powerball_object(draw_date='2026-02-28', draw_result={'main_balls': [6, 20, 35, 54, 65], 'powerball': 10, 'multiplier': 4})
|
||||
PowerballDraw(draw_date=datetime(2026, 2, 28), main_ball1=6, main_ball2=20, main_ball3=35, main_ball4=54, main_ball5=65, powerball=10, power_play=4)
|
||||
>>> get_powerball_object(draw_date='2026-02-28', draw_result={
|
||||
'main_balls': [6, 20, 35, 54, 65], 'powerball': 10, 'multiplier': 4})
|
||||
PowerballDraw(draw_date=datetime(2026, 2, 28), main_ball1=6,
|
||||
main_ball2=20, main_ball3=35, main_ball4=54, main_ball5=65, powerball=10,
|
||||
power_play=4)
|
||||
"""
|
||||
draw_date = datetime.strptime(draw_date, '%Y-%m-%d').date()
|
||||
mb1, mb2, mb3, mb4, mb5 = draw_result['main_balls']
|
||||
pb = draw_result['powerball']
|
||||
power_play = draw_result['multiplier']
|
||||
|
||||
return PowerballDraw(draw_date=draw_date,main_ball1=mb1, main_ball2=mb2, main_ball3=mb3, main_ball4=mb4,
|
||||
main_ball5=mb5, powerball=pb, power_play=power_play)
|
||||
return PowerballDraw(
|
||||
draw_date=draw_date, main_ball1=mb1, main_ball2=mb2, main_ball3=mb3,
|
||||
main_ball4=mb4,
|
||||
main_ball5=mb5, powerball=pb, power_play=power_play,
|
||||
)
|
||||
|
||||
|
||||
def get_mega_millions_object(draw_date: str, draw_result: dict) -> MegaMillionsDraw:
|
||||
def get_mega_millions_object(
|
||||
draw_date: str, draw_result: dict,
|
||||
) -> MegaMillionsDraw:
|
||||
"""
|
||||
:param draw_date: date for the drawing data
|
||||
:param draw_result: drawing results for the draw_date
|
||||
:return: MegaMillionsDraw object
|
||||
|
||||
Examples:
|
||||
>>> get_mega_millions_object(draw_date='2010-02-02', draw_result={'main_balls': [6, 7, 26, 27, 49], 'mega-ball': 9, 'megaplier': 4})
|
||||
MegaMillionsDraw(draw_date=date, main_ball1=6, main_ball2=7, main_ball3=26, main_ball4=27, main_ball5=49, mega_ball=9, megaplier=4)
|
||||
>>> get_mega_millions_object(draw_date='2010-02-02', draw_result={
|
||||
'main_balls': [6, 7, 26, 27, 49], 'mega-ball': 9, 'megaplier': 4})
|
||||
MegaMillionsDraw(draw_date=date, main_ball1=6, main_ball2=7,
|
||||
main_ball3=26, main_ball4=27, main_ball5=49, mega_ball=9, megaplier=4)
|
||||
"""
|
||||
draw_date = datetime.strptime(draw_date, '%Y-%m-%d').date()
|
||||
mb1, mb2, mb3, mb4, mb5 = draw_result['main_balls']
|
||||
mega = draw_result['mega-ball']
|
||||
megaplier = draw_result['multiplier']
|
||||
|
||||
return MegaMillionsDraw(draw_date=draw_date, main_ball1=mb1, main_ball2=mb2, main_ball3=mb3, main_ball4=mb4,
|
||||
main_ball5=mb5, mega_ball=mega, megaplier=megaplier)
|
||||
return MegaMillionsDraw(
|
||||
draw_date=draw_date, main_ball1=mb1, main_ball2=mb2, main_ball3=mb3,
|
||||
main_ball4=mb4,
|
||||
main_ball5=mb5, mega_ball=mega, megaplier=megaplier,
|
||||
)
|
||||
@@ -4,10 +4,13 @@
|
||||
Utility function for loading project environment variables
|
||||
"""
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def load_environment_variables() -> dict:
|
||||
return {
|
||||
k: v for k, v in os.environ.items()
|
||||
|
||||
+29
-12
@@ -9,15 +9,18 @@ from math import comb
|
||||
|
||||
def calculate_odds(pool_size: int, draw_count: int) -> int:
|
||||
"""
|
||||
Calculate the total number of possible combinations in a set of numbers with a specific draw count.
|
||||
Calculate the total number of possible combinations in a set of numbers
|
||||
with a specific draw count.
|
||||
|
||||
:param pool_size: Total count of numbers in the pool
|
||||
:param draw_count: The count of numbers to draw from the pool
|
||||
:return: The total number of possible combinations
|
||||
|
||||
Example:
|
||||
If a lottery game has a main ball pool of 49 numbers from which 5 numbers are drawn and a pool of
|
||||
18 numbers from which 1 number is drawn (game ball), calculate the odds of winning that game.
|
||||
If a lottery game has a main ball pool of 49 numbers from which 5
|
||||
numbers are drawn and a pool of
|
||||
18 numbers from which 1 number is drawn (game ball), calculate the
|
||||
odds of winning that game.
|
||||
|
||||
>>> calculate_odds(pool_size=49, draw_count=5)
|
||||
1906884
|
||||
@@ -26,7 +29,10 @@ def calculate_odds(pool_size: int, draw_count: int) -> int:
|
||||
"""
|
||||
|
||||
if draw_count < 0 or draw_count > pool_size:
|
||||
raise ValueError('Integer values must be positive and draw counts must be less than pool size.')
|
||||
raise ValueError(
|
||||
'Integer values must be positive and draw counts must be less '
|
||||
'than pool size.',
|
||||
)
|
||||
total_combinations = comb(pool_size, draw_count)
|
||||
return total_combinations
|
||||
|
||||
@@ -35,29 +41,40 @@ def get_odds_of_winning(
|
||||
main_ball_draw_pool: int,
|
||||
main_ball_draw_count: int,
|
||||
game_ball_draw_pool: int,
|
||||
game_ball_draw_count: int
|
||||
game_ball_draw_count: int,
|
||||
) -> str:
|
||||
"""
|
||||
Calculate the odds of winning a specific lottery game with one draw
|
||||
|
||||
:param main_ball_draw_pool: Total count of numbers in the main ball pool
|
||||
:param main_ball_draw_count: The count of numbers to draw from the main ball pool
|
||||
:param main_ball_draw_count: The count of numbers to draw from the
|
||||
main ball pool
|
||||
:param game_ball_draw_pool: Total count of numbers in the game ball pool
|
||||
:param game_ball_draw_count: The count of numbers to draw from the game ball pool
|
||||
:param game_ball_draw_count: The count of numbers to draw from the
|
||||
game ball pool
|
||||
|
||||
:return: The odds of winning a specific lottery game with one draw
|
||||
|
||||
Example:
|
||||
To calculate the odds of winning a lottery game with 5 numbers drawn from a main ball pool of 49 and
|
||||
To calculate the odds of winning a lottery game with 5 numbers
|
||||
drawn from a main ball pool of 49 and
|
||||
1 number drawn from a game ball pool of 19 numbers.
|
||||
|
||||
>>> get_odds_of_winning(main_ball_draw_pool=49, main_ball_draw_count=5, game_ball_draw_pool=18, game_ball_draw_count=1)
|
||||
>>> get_odds_of_winning(main_ball_draw_pool=49,
|
||||
main_ball_draw_count=5, game_ball_draw_pool=18,
|
||||
game_ball_draw_count=1)
|
||||
'The odds of winning are 1 in 34,323,912'
|
||||
"""
|
||||
|
||||
if (main_ball_draw_count < 0 or main_ball_draw_count > main_ball_draw_pool or
|
||||
game_ball_draw_count < 0 or game_ball_draw_count > game_ball_draw_pool):
|
||||
raise ValueError('Integer values must be positive and draw counts must be less than pool size.')
|
||||
if (
|
||||
main_ball_draw_count < 0 or main_ball_draw_count >
|
||||
main_ball_draw_pool or
|
||||
game_ball_draw_count < 0 or game_ball_draw_count >
|
||||
game_ball_draw_pool):
|
||||
raise ValueError(
|
||||
'Integer values must be positive and draw counts must be less '
|
||||
'than pool size.',
|
||||
)
|
||||
|
||||
main_ball_odds = calculate_odds(main_ball_draw_pool, main_ball_draw_count)
|
||||
game_ball_odds = calculate_odds(game_ball_draw_pool, game_ball_draw_count)
|
||||
|
||||
Regular → Executable
+44
-15
@@ -1,22 +1,26 @@
|
||||
"""
|
||||
util/scrape.py
|
||||
|
||||
Utility functions for scaping web data relative to selected games
|
||||
Utility functions for scraping web data relative to selected games
|
||||
"""
|
||||
import httpx
|
||||
from dateutil import parser, utils
|
||||
from bs4 import BeautifulSoup
|
||||
from dateutil import parser, utils
|
||||
|
||||
from util.environment import load_environment_variables
|
||||
|
||||
|
||||
project_variables: dict = load_environment_variables()
|
||||
|
||||
|
||||
def scrape_game_history(game: str, year: int = utils.today().year) -> dict:
|
||||
"""
|
||||
Scrape game data from the url provided. This function requires specific environment variables be present
|
||||
Scrape game data from the url provided. This function requires
|
||||
specific environment variables be present
|
||||
in order to function properly.
|
||||
:param:game The target game to scrape.
|
||||
:param:year The year to scrape. If no year is provided, scrapes games from the current year.
|
||||
:param:year The year to scrape. If no year is provided, scrapes
|
||||
games from the current year.
|
||||
|
||||
:return: dict()
|
||||
|
||||
@@ -32,18 +36,37 @@ def scrape_game_history(game: str, year: int = utils.today().year) -> dict:
|
||||
url = f"{project_variables['LP_BASE_SCRAPE_URL']}{game}/numbers/{year}"
|
||||
page = httpx.get(url)
|
||||
soup = BeautifulSoup(page.text, 'html.parser')
|
||||
results = soup.find_all(project_variables['LP_RESULT_CONTAINER'], class_=project_variables['LP_RESULT_CLASS'])
|
||||
results = soup.find_all(
|
||||
project_variables['LP_RESULT_CONTAINER'],
|
||||
class_=project_variables['LP_RESULT_CLASS'],
|
||||
)
|
||||
|
||||
for result in results:
|
||||
pick5 = []
|
||||
draw_date = parser.parse(result.find(project_variables['LP_DRAW_DATE_CONTAINER'],
|
||||
class_=project_variables['LP_DRAW_DATE_CLASS']).text).date().strftime('%Y-%m-%d')
|
||||
main_balls = result.find_all(project_variables['LP_BALL_CONTAINER'], class_=project_variables['LP_BALL_CLASS'])
|
||||
game_ball = result.find(project_variables['LP_BALL_CONTAINER'],
|
||||
class_=[project_variables['LP_POWERBALL_CLASS'], project_variables['LP_MEGABALL_CLASS']])
|
||||
multiplier = result.find(project_variables['LP_BALL_CONTAINER'],
|
||||
class_=[project_variables['LP_POWERBALL_MULTIPLIER_CLASS'],
|
||||
project_variables['LP_MEGA_MILLIONS_MULTIPLIER_CLASS']])
|
||||
draw_date = parser.parse(
|
||||
result.find(
|
||||
project_variables['LP_DRAW_DATE_CONTAINER'],
|
||||
class_=project_variables['LP_DRAW_DATE_CLASS'],
|
||||
).text,
|
||||
).date().strftime('%Y-%m-%d')
|
||||
main_balls = result.find_all(
|
||||
project_variables['LP_BALL_CONTAINER'],
|
||||
class_=project_variables['LP_BALL_CLASS'],
|
||||
)
|
||||
game_ball = result.find(
|
||||
project_variables['LP_BALL_CONTAINER'],
|
||||
class_=[
|
||||
project_variables['LP_POWERBALL_CLASS'],
|
||||
project_variables['LP_MEGABALL_CLASS'],
|
||||
],
|
||||
)
|
||||
multiplier = result.find(
|
||||
project_variables['LP_BALL_CONTAINER'],
|
||||
class_=[
|
||||
project_variables['LP_POWERBALL_MULTIPLIER_CLASS'],
|
||||
project_variables['LP_MEGA_MILLIONS_MULTIPLIER_CLASS'],
|
||||
],
|
||||
)
|
||||
|
||||
for ball in main_balls:
|
||||
pick5.append(int(ball.text.strip()))
|
||||
@@ -55,8 +78,14 @@ def scrape_game_history(game: str, year: int = utils.today().year) -> dict:
|
||||
multiplier = 1
|
||||
|
||||
if game == 'mega-millions':
|
||||
drawings[draw_date] = {'main_balls': pick5, 'mega-ball': game_ball, 'multiplier': multiplier}
|
||||
drawings[draw_date] = {
|
||||
'main_balls': pick5, 'mega-ball': game_ball,
|
||||
'multiplier': multiplier,
|
||||
}
|
||||
elif game == 'powerball':
|
||||
drawings[draw_date] = {'main_balls': pick5, 'powerball': game_ball, 'multiplier': multiplier}
|
||||
drawings[draw_date] = {
|
||||
'main_balls': pick5, 'powerball': game_ball,
|
||||
'multiplier': multiplier,
|
||||
}
|
||||
|
||||
return drawings
|
||||
@@ -217,6 +217,8 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "sqlalchemy-utils" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -235,6 +237,8 @@ requires-dist = [
|
||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||
{ name = "scikit-learn", specifier = ">=1.8.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.46" },
|
||||
{ name = "sqlalchemy-utils", specifier = ">=0.42.1" },
|
||||
{ name = "tqdm", specifier = ">=4.67.3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -522,6 +526,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy-utils"
|
||||
version = "0.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/7d/eb9565b6a49426552a5bf5c57e7c239c506dc0e4e5315aec6d1e8241dc7c/sqlalchemy_utils-0.42.1.tar.gz", hash = "sha256:881f9cd9e5044dc8f827bccb0425ce2e55490ce44fc0bb848c55cc8ee44cc02e", size = 130789, upload-time = "2025-12-13T03:14:13.591Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/25/7400c18c3ee97914cc99c90007795c00a4ec5b60c853b49db7ba24d11179/sqlalchemy_utils-0.42.1-py3-none-any.whl", hash = "sha256:243cfe1b3a1dae3c74118ae633f1d1e0ed8c787387bc33e556e37c990594ac80", size = 91761, upload-time = "2025-12-13T03:14:15.014Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threadpoolctl"
|
||||
version = "3.6.0"
|
||||
@@ -531,6 +547,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
|
||||
Reference in new issue
Block a user