├── .gitignore ├── LICENSE ├── ab-testing ├── README.md └── demo.ipynb ├── anthropic-fastapi ├── README.md ├── requirements.txt ├── scripts │ └── launch_uvicorn.bash ├── src │ ├── llm │ │ ├── anthropic_helpers.py │ │ ├── embeddings.py │ │ └── schemas.py │ ├── load_config.py │ ├── main.py │ └── startup │ │ ├── app.py │ │ ├── database.py │ │ ├── test_routes.py │ │ └── throttle.py └── startup.sh ├── causal-simulation-dowhy ├── causal_model.png └── demo.ipynb ├── edatk ├── README.md ├── assets │ ├── edatk_chart_multi_variable_10_13_2021_02_11_36_571742.png │ ├── edatk_charts_multi_variable_10_13_2021_02_11_35_826709.png │ ├── edatk_charts_single_variable_10_13_2021_02_11_30_369170.png │ ├── edatk_charts_single_variable_10_13_2021_02_11_31_674656.png │ ├── edatk_charts_single_variable_10_13_2021_02_11_33_068926.png │ ├── edatk_charts_single_variable_10_13_2021_02_11_34_567949.png │ └── edatk_charts_single_variable_10_13_2021_02_11_34_972858.png ├── demo.ipynb └── report.html ├── flask-api ├── README.md ├── pbi │ ├── powerquery_code.pq │ └── test_data.csv └── python_logic │ ├── API │ ├── api_operations.py │ └── housing_clustering.py │ ├── Schemas │ ├── request_schemas.py │ └── response_schemas.py │ ├── SimpleRecommendation.py │ └── Starter │ └── app.py ├── hyperparameter-optimization ├── README.md └── optuna │ ├── demo.ipynb │ └── winemag-data-130k-v2.csv ├── nlp └── preprocess │ ├── README.md │ ├── d3_animations │ ├── customstyles.css │ ├── helpers │ │ ├── chart.js │ │ ├── dimension.js │ │ └── structure_builders.js │ ├── index.html │ ├── lemmatize.js │ ├── stem.js │ ├── stopwords.js │ └── tokenization.js │ ├── data_source.txt │ ├── demo.ipynb │ └── the_wind_cries_mary.txt ├── outlier-detection └── isolation-forest │ ├── README.md │ └── demo.ipynb ├── pydantic-ai-streaming ├── README.md ├── requirements.txt ├── scripts │ ├── base_model_test.ipynb │ └── pydantic_ai_base_test.ipynb └── src │ ├── base_stream_example.py │ ├── pydantic_stream_example.py │ └── schemas.py ├── sarimax ├── README.md ├── data │ ├── Kansas_City_Crime__NIBRS__Summary.csv │ ├── final_results.csv │ └── prelim_data.csv ├── pbi │ └── results.pbix ├── python-logic │ ├── helpers.py │ └── seasonal_decomp_and_arima.py └── requirements.txt ├── seasonal-decomp ├── README.md ├── data │ ├── Kansas_City_Crime__NIBRS__Summary.csv │ └── results.csv ├── pbi │ └── results.pbix ├── python-logic │ ├── helpers.py │ └── seasonal_decomp.py └── requirements.txt ├── simple-mcp-example ├── README.md ├── requirements.txt ├── scripts │ ├── launch_uvicorn.bash │ └── pydantic_ai_base_test.ipynb └── src │ ├── build_mcp │ └── create_mcp.py │ ├── load_config.py │ ├── main.py │ ├── startup │ ├── app.py │ ├── mcp.py │ └── routes.py │ └── test_mcp │ ├── client.py │ └── pydantic_stream_example.py ├── sliced └── 2021_01 │ ├── .ipynb_checkpoints │ └── Sliced_Week_01-checkpoint.ipynb │ ├── Competition.txt │ ├── README.md │ ├── Sliced_Week_01.ipynb │ ├── results.PNG │ ├── sample_submission.csv │ ├── submissions │ ├── submission.csv │ ├── submission2.csv │ ├── submission3.csv │ ├── submission4.csv │ ├── submission5.csv │ └── submission6.csv │ ├── test.csv │ └── train.csv ├── smote ├── README.md └── demo.ipynb ├── time-series-eda ├── data_download.py └── demo.ipynb ├── time-series-resample ├── README.md ├── daily_sample.csv └── demo.ipynb ├── vectorized_linear_regression ├── README.md └── demo.ipynb └── vue-chat-frontend ├── .editorconfig ├── README.md ├── env.d.ts ├── eslint.config.ts ├── index.html ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── css │ │ └── main.css │ └── logo.svg ├── components │ ├── backgrounds │ │ └── BackgroundWrapper.vue │ ├── chat │ │ └── ChatPageStructure.vue │ └── icons │ │ ├── IconCommunity.vue │ │ ├── IconDocumentation.vue │ │ ├── IconEcosystem.vue │ │ ├── IconSupport.vue │ │ └── IconTooling.vue ├── composables │ ├── useCache.ts │ └── useStreamingAPI.ts ├── devlogger │ └── devlogger.ts ├── main.ts ├── router │ └── index.ts ├── types │ └── chatTypes.ts └── views │ ├── ChatPage.vue │ └── PageNotFound.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | flask-api/.vscode/launch.json 2 | flask-api/.vscode/settings.json 3 | flask-api/.pylintrc 4 | flask-api/pbi/pbi_api_ml.pbix 5 | flask-api/python_logic/test_api.txt 6 | flask-api/python_logic/PA_Only/config.py 7 | flask-api/python_logic/testing.py 8 | seasonal-decomp/.pylintrc 9 | seasonal-decomp/.vscode/launch.json 10 | seasonal-decomp/.vscode/settings.json 11 | requirements gen.txt 12 | seasonal-decomp/python-logic/__pycache__/helpers.cpython-38.pyc 13 | sarimax/.pylintrc 14 | sarimax/.vscode/launch.json 15 | sarimax/.vscode/settings.json 16 | sarimax/python-logic/__pycache__/helpers.cpython-38.pyc 17 | hyperparameter-optimization/optuna/catboost_info/* 18 | nlp/preprocess/.vector_cache/* 19 | connect-four/__pycache__/* 20 | connect-four/src/__pycache__/* 21 | connect-four/web/config.py 22 | connect-four/* 23 | boosted-tree-time-series/data/* 24 | boosted-tree-time-series/catboost_info 25 | time-series-eda/data/* 26 | boosted-tree-time-series/demo.ipynb 27 | boosted-tree-time-series/data_download.ipynb 28 | boosted-tree-time-series/* 29 | .env 30 | pydantic-ai-streaming/.env 31 | pydantic-ai-streaming/.vscode/* 32 | pydantic-ai-streaming/.vscode/ 33 | __pycache__/ 34 | *.py[cod] 35 | *$py.class 36 | simple-mcp-example/.env 37 | simple-mcp-example/.vscode/* 38 | simple-mcp-example/.vscode/ -------------------------------------------------------------------------------- /ab-testing/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/ab-testing). -------------------------------------------------------------------------------- /ab-testing/demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# AB Testing\n", 8 | "This notebook accompanies a blog article, overviewing the basics of AB Testing." 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "## Imports and seed reproducability" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 15, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "from math import ceil\n", 25 | "import numpy as np\n", 26 | "import pandas as pd\n", 27 | "import statsmodels.api as sm\n", 28 | "import seaborn as sns\n", 29 | "from statsmodels.stats.proportion import proportions_ztest\n", 30 | "sns.set_theme()" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "np.random.seed(seed=42)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## Effect and Sample Size\n", 47 | "Before gathering data, recommended to calculate effect and sample size." 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "### Effect Size" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 13, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "For a change from 0.70 to 0.75, effect size is -0.11.\n" 67 | ] 68 | } 69 | ], 70 | "source": [ 71 | "init_prop = 0.7\n", 72 | "mde_prop = 0.75\n", 73 | "effect_size = sm.stats.proportion_effectsize(init_prop, mde_prop)\n", 74 | "print(f'For a change from {init_prop:.2f} to {mde_prop:.2f}, effect size is {effect_size:.2f}.')" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "### Sample Size" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 18, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "name": "stdout", 91 | "output_type": "stream", 92 | "text": [ 93 | "1250 sample size required given power analysis and input parameters.\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "sample_size = sm.stats.zt_ind_solve_power(effect_size=effect_size, nobs1=None, alpha=0.05, power=0.8)\n", 99 | "print(f'{ceil(sample_size)} sample size required given power analysis and input parameters.')" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "## Data Generation\n", 107 | "For this example, we'll use dummy data to represent changes in a video game" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "data_generated_a = np.random.choice([0,1], size=1250, replace=True, p=[0.20, 0.8])\n", 117 | "data_generated_b = np.random.choice([0,1], size=1250, replace=True, p=[0.3, 0.70])" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 6, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "df = pd.DataFrame({'medium': data_generated_a, 'hard': data_generated_b})" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 7, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "data": { 136 | "text/plain": [ 137 | "medium 980\n", 138 | "hard 881\n", 139 | "dtype: int64" 140 | ] 141 | }, 142 | "execution_count": 7, 143 | "metadata": {}, 144 | "output_type": "execute_result" 145 | } 146 | ], 147 | "source": [ 148 | "df.sum()" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 8, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "image/png": "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", 159 | "text/plain": [ 160 | "
" 161 | ] 162 | }, 163 | "metadata": {}, 164 | "output_type": "display_data" 165 | } 166 | ], 167 | "source": [ 168 | "# Plot initial data and format\n", 169 | "plot_df = df.melt()\n", 170 | "plot_df.columns = ['Difficulty', 'User_Continues']\n", 171 | "ax = sns.countplot(x='Difficulty', hue='User_Continues', data=plot_df)\n", 172 | "ax.set_title(\"Medium vs. Hard Difficulty - Whether User Continues or Not\")\n", 173 | "ax.legend(['Stops', 'Continues'], loc='upper right')\n", 174 | "ax.set_ybound(0, 1500)\n", 175 | "for p in ax.patches:\n", 176 | " x=p.get_bbox().get_points()[:,0].mean()\n", 177 | " y=p.get_bbox().get_points()[1,1]\n", 178 | " ax.annotate(f'{y:.0f}', (x,y+5), ha='center', va='bottom', color=p.get_facecolor())" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "## Analyze Results" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 12, 191 | "metadata": {}, 192 | "outputs": [ 193 | { 194 | "name": "stdout", 195 | "output_type": "stream", 196 | "text": [ 197 | "Medium: 980 successes of 1250 trials. Hard: 881 successes of 1250 trials.\n" 198 | ] 199 | } 200 | ], 201 | "source": [ 202 | "# Get z test requirements\n", 203 | "medium_successes = df.loc[:, 'medium'].sum()\n", 204 | "hard_successes = df.loc[:, 'hard'].sum()\n", 205 | "medium_trials = df.loc[:, 'medium'].count()\n", 206 | "hard_trials = df.loc[:, 'hard'].count()\n", 207 | "\n", 208 | "print(f'Medium: {medium_successes} successes of {medium_trials} trials. Hard: {hard_successes} successes of {hard_trials} trials.')" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 10, 214 | "metadata": {}, 215 | "outputs": [ 216 | { 217 | "name": "stdout", 218 | "output_type": "stream", 219 | "text": [ 220 | "z stat of 4.539 and p value of 0.0000.\n" 221 | ] 222 | } 223 | ], 224 | "source": [ 225 | "# Perform z test\n", 226 | "z_stat, p_value = proportions_ztest(count=[medium_successes, hard_successes], nobs=[medium_trials, hard_trials])\n", 227 | "print(f'z stat of {z_stat:.3f} and p value of {p_value:.4f}.')" 228 | ] 229 | } 230 | ], 231 | "metadata": { 232 | "interpreter": { 233 | "hash": "557bea64a4d33a54b4028138dfa5f7ef28b1855e4803cd7cef7b3f2a14105b37" 234 | }, 235 | "kernelspec": { 236 | "display_name": "Python 3.9.6 64-bit ('sliced2': conda)", 237 | "name": "python3" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 3 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython3", 249 | "version": "3.9.6" 250 | }, 251 | "orig_nbformat": 4 252 | }, 253 | "nbformat": 4, 254 | "nbformat_minor": 2 255 | } 256 | -------------------------------------------------------------------------------- /anthropic-fastapi/README.md: -------------------------------------------------------------------------------- 1 | ## Anthropic Streaming + Fast API 2 | Simple Fast API Backend for Anthropic Streaming llm example. For frontent example, see vue-chat-frontend. -------------------------------------------------------------------------------- /anthropic-fastapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn[standard] 3 | gunicorn 4 | python-jose[cryptography] 5 | passlib[bcrypt] 6 | python-dotenv 7 | aioodbc 8 | python-multipart 9 | tenacity 10 | pandas 11 | pytz 12 | slowapi 13 | sendgrid 14 | anthropic 15 | langchain[anthropic] 16 | sentence-transformers -------------------------------------------------------------------------------- /anthropic-fastapi/scripts/launch_uvicorn.bash: -------------------------------------------------------------------------------- 1 | uvicorn src.main:app --reload -------------------------------------------------------------------------------- /anthropic-fastapi/src/llm/anthropic_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from typing import AsyncGenerator 4 | from anthropic import AsyncAnthropic 5 | 6 | ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY') 7 | 8 | 9 | async def anthropic_stream_api_call(chat_input_list: list) -> AsyncGenerator[str, None]: 10 | system_prompt_text = 'You are an AI agent.' 11 | 12 | # Build message list 13 | message_input = [] 14 | for chat_instance in chat_input_list: 15 | print(chat_instance) 16 | 17 | message_input.append({ 18 | 'role': chat_instance['role'], 19 | "content": [ 20 | { 21 | "type": "text", 22 | "text": chat_instance['message'] 23 | } 24 | ] 25 | }) 26 | 27 | # Setup and make api call. 28 | client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY) 29 | stream = await client.messages.create( 30 | model="claude-3-haiku-20240307", 31 | max_tokens=2000, 32 | system=system_prompt_text, 33 | messages=message_input, 34 | stream=True 35 | ) 36 | 37 | async for event in stream: 38 | if event.type in ['message_start', 'message_delta', 'message_stop', 'content_block_start', 'content_block_stop']: 39 | pass 40 | elif event.type == 'content_block_delta': 41 | yield event.delta.text 42 | else: 43 | yield event.type 44 | 45 | -------------------------------------------------------------------------------- /anthropic-fastapi/src/llm/embeddings.py: -------------------------------------------------------------------------------- 1 | from sentence_transformers import SentenceTransformer 2 | from sentence_transformers.util import semantic_search 3 | 4 | 5 | model = SentenceTransformer( 6 | "intfloat/e5-small-v2", 7 | cache_folder='src/model_download_cache' 8 | ) 9 | print(model) 10 | 11 | 12 | def get_top_k_results(queries: list[str], passages: list[str], top_k: int = 3) -> list[str]: 13 | """Get top k results using text embedding model. 14 | 15 | Args: 16 | queries (list[str]): Questions or queries to use to lookup. 17 | passages (list[str]): Possible results. 18 | 19 | Returns: 20 | list[str]: List of unique passages that best match the query, up to top_k matches per query. 21 | """ 22 | input_texts = ['query: ' + query for query in queries] + ['passage: ' + passage for passage in passages] 23 | embeddings = model.encode(input_texts, normalize_embeddings=True) 24 | query_embeddings = embeddings[:len(queries)] 25 | passage_embeddings = embeddings[len(queries):] 26 | 27 | search_results = semantic_search(query_embeddings, passage_embeddings, top_k=top_k) 28 | text_only_result_list = [] 29 | for result in search_results: 30 | for result_match in result: 31 | if passages[result_match['corpus_id']] not in text_only_result_list: 32 | text_only_result_list.append(passages[result_match['corpus_id']]) 33 | 34 | return text_only_result_list 35 | -------------------------------------------------------------------------------- /anthropic-fastapi/src/llm/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ChatInput(BaseModel): 5 | chat_input_list: list 6 | 7 | class Config: 8 | json_schema_extra = { 9 | "example": { 10 | "chat_input_list": [ 11 | { 12 | "role": "user", 13 | "message": "What is your name?" 14 | }, 15 | { 16 | "role": "assistant", 17 | "message": "My name is Claude" 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /anthropic-fastapi/src/load_config.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | 3 | load_dotenv() -------------------------------------------------------------------------------- /anthropic-fastapi/src/main.py: -------------------------------------------------------------------------------- 1 | from src.load_config import * 2 | from src.startup.app import * 3 | from src.llm.embeddings import * 4 | from src.startup.test_routes import * -------------------------------------------------------------------------------- /anthropic-fastapi/src/startup/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, HTTPException, status 2 | from fastapi.responses import JSONResponse 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | # Start up app 6 | app = FastAPI() 7 | 8 | # Only only specific origins 9 | app.add_middleware( 10 | CORSMiddleware, 11 | allow_origin_regex="https?://localhost(:\d+)?", # Could add other sites as needed |^https://dev--\.netlify\.app$ 12 | allow_credentials=True, 13 | allow_methods=["GET","POST"], 14 | allow_headers=[ 15 | "Content-Type", 16 | "Authorization", 17 | "Accept", 18 | "Origin", 19 | "X-Requested-With" 20 | ], 21 | ) 22 | 23 | 24 | @app.middleware('http') 25 | async def validate_referer(request: Request, call_next): 26 | 27 | # Check Referer 28 | approved_referers = ['http://localhost:8000/', 'http://localhost:8080/', 'http://localhost:5173/'] # Could later add 'https://xxx.netlify.app/', 'https://xxx.azurewebsites.net/' 29 | referer = request.headers.get('referer') 30 | if referer: 31 | if not any(referer.startswith(approved_referer) for approved_referer in approved_referers): 32 | raise HTTPException( 33 | status_code=status.HTTP_400_BAD_REQUEST, 34 | detail="Not Authorized", 35 | headers={"WWW-Authenticate": "Bearer"} 36 | ) 37 | 38 | response = await call_next(request) 39 | return response 40 | 41 | """ # Enable if db is needed 42 | from src.startup.database import create_pool, close_pool 43 | 44 | @app.on_event("startup") 45 | async def startup(): 46 | await create_pool() 47 | 48 | 49 | @app.on_event("shutdown") 50 | async def shutdown(): 51 | await close_pool() 52 | """ -------------------------------------------------------------------------------- /anthropic-fastapi/src/startup/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | import aioodbc 3 | import asyncio 4 | from functools import wraps 5 | from fastapi import HTTPException 6 | 7 | DB_SERVER = os.getenv('DB_SERVER') 8 | DB_USER_NAME = os.getenv('DB_USER_NAME') 9 | DB_USER_PW = os.getenv('DB_USER_PW') 10 | DB_DATABASE = os.getenv('DB_DATABASE') 11 | 12 | 13 | # Create a connection pool 14 | dsn=f'DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={DB_SERVER};PORT=1433;DATABASE={DB_DATABASE};UID={DB_USER_NAME};PWD={DB_USER_PW};Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;' 15 | pool = None 16 | 17 | 18 | async def create_pool(): 19 | """Create a pool to execute later queries under. 20 | """ 21 | global pool 22 | pool = await aioodbc.create_pool(dsn=dsn, pool_recycle=25*60) 23 | 24 | 25 | async def close_pool(): 26 | """Close out the pool before shutting down. 27 | """ 28 | global pool 29 | pool.close() 30 | await pool.wait_closed() 31 | 32 | 33 | def retry_sql_query(retries=3, delay=1): 34 | """Retry sql query in case of bad connections. TODO: Implement more specific error catching and verify decorator fully async. 35 | 36 | Args: 37 | retries (int, optional): Number of retry attempts. Defaults to 3. 38 | delay (int, optional): How many seconds to wait before retry. Defaults to 1. 39 | """ 40 | def decorator(func): 41 | @wraps(func) 42 | async def wrapper(*args, **kwargs): 43 | for _ in range(retries): 44 | try: 45 | return await func(*args, **kwargs) 46 | except Exception as e: 47 | print(f'Retry on error {e}') 48 | if _ == retries - 1: 49 | raise e 50 | await asyncio.sleep(delay) 51 | return wrapper 52 | return decorator 53 | 54 | 55 | @retry_sql_query() 56 | async def run_query(sql_query_statement: str, params: list = None, query_context: str = None, extra_tabs: int = 0, exec_many: bool = False) -> list[dict]: 57 | """Run a query against database pool. TODO: Change all prints to logging. 58 | 59 | Args: 60 | sql_query_statement (str): Query statement (ex: SELECT * FROM schema.table WHERE id = ?). 61 | params (optional, list): list of params to pipe into the query. 62 | query_context (optional, str): String representing context of the query, for displaying/logging/debug. 63 | extra_tabs (optional, int): Number of extra tabs to insert. Defaults to 0. 64 | exec_many (optional, bool): Whether to exec many or single. Defaults to false (exec single). 65 | 66 | Raises: 67 | HTTPException: Exception if the query failed. 68 | 69 | Returns: 70 | list[dict]: List of rows returned in column_name:column_value dict format. 71 | """ 72 | if query_context is not None: 73 | print('\n' + '\t'*extra_tabs + '+'*10 + f' Running Select Query {query_context} ' + '+'*10) 74 | else: 75 | print('\n' + '\t'*extra_tabs + '+'*10 + ' Running Select Query ' + '+'*10) 76 | 77 | try: 78 | async with pool.acquire() as connection: 79 | async with connection.cursor() as cursor: 80 | print('\t'*extra_tabs + 'Executing Query...') 81 | print('\t'*extra_tabs + sql_query_statement) 82 | if params is not None: 83 | if exec_many: 84 | batch_size = 1000 85 | for i in range(0, len(params), batch_size): 86 | batch_params = params[i:i + batch_size] 87 | await cursor.executemany(sql_query_statement, batch_params) 88 | else: 89 | print('\t'*extra_tabs + f"Query Params: {','.join([str(param) for param in params])}.") 90 | await cursor.execute(sql_query_statement, params) 91 | else: 92 | await cursor.execute(sql_query_statement) 93 | if cursor.description: 94 | column_names = [column[0] for column in cursor.description] 95 | rows = await cursor.fetchall() 96 | result = [dict(zip(column_names, row)) for row in rows] 97 | print('\t'*extra_tabs + '-'*10 + ' Select Query Completed ' + '-'*10) 98 | return result 99 | else: 100 | print('\t'*extra_tabs + '-'*10 + ' Select Query Completed ' + '-'*10) 101 | return True 102 | except Exception as e: 103 | print('\t'*extra_tabs + f"Error occurred during database query: {e}") 104 | raise HTTPException(status_code=500, detail="An error occurred during the database query.") 105 | 106 | 107 | @retry_sql_query() 108 | async def execute_store_procedure(stored_procedure_name: str, params: dict, extra_tabs: int = 0) -> list[dict]: 109 | """Execute Parameterized Stored Proc. TODO: Change all prints to logging. 110 | 111 | Args: 112 | stored_procedure_name (str): Stored procedure name. 113 | params (dict): Params in the param_name:param_value dict format. 114 | extra_tabs (optional, int): Number of extra tabs to insert. Defaults to 0. 115 | 116 | Raises: 117 | HTTPException: Exception if proc failed. 118 | 119 | Returns: 120 | list[dict]: List of rows returned in column_name:column_value dict format. 121 | """ 122 | print('\n' + '\t'*extra_tabs + '+'*10 + f' Running Stored Proc {stored_procedure_name} ' + '+'*10) 123 | try: 124 | async with pool.acquire() as connection: 125 | async with connection.cursor() as cursor: 126 | sql_query_statement = f"EXEC {stored_procedure_name} " + ", ".join([f"@{name} = ?" for name in params]) 127 | print('\t'*extra_tabs + sql_query_statement) 128 | print('\t'*extra_tabs) 129 | print(*params.values()) 130 | await cursor.execute(sql_query_statement, *params.values()) 131 | if cursor.description: 132 | column_names = [column[0] for column in cursor.description] 133 | rows = await cursor.fetchall() 134 | result = [dict(zip(column_names, row)) for row in rows] 135 | print('\t'*extra_tabs + '-'*10 + ' Stored Proc Completed ' + '-'*10) 136 | return result 137 | else: 138 | print('\t'*extra_tabs + '-'*10 + ' Stored Proc Completed ' + '-'*10) 139 | return True 140 | except Exception as e: 141 | print('\t'*extra_tabs + f"Error occurred during database query: {e}") 142 | raise HTTPException(status_code=500, detail="An error occurred during the database query.") 143 | -------------------------------------------------------------------------------- /anthropic-fastapi/src/startup/test_routes.py: -------------------------------------------------------------------------------- 1 | from src.startup.app import app 2 | from src.startup.throttle import limiter 3 | from fastapi import Request, Response 4 | from fastapi.responses import StreamingResponse 5 | from src.llm.anthropic_helpers import anthropic_stream_api_call 6 | from src.llm.schemas import ChatInput 7 | 8 | 9 | @app.get("/") 10 | def read_root(): 11 | return {"Hello": "World2"} 12 | 13 | 14 | @app.get("/throttle") 15 | @limiter.limit("10/minute") 16 | def throttle_test(request: Request): 17 | return {"Hello": "World2-Throttle"} 18 | 19 | 20 | @app.post("/stream") 21 | async def stream_text(input: ChatInput): 22 | """FastAPI endpoint to stream AI-generated text""" 23 | return StreamingResponse(anthropic_stream_api_call(input.chat_input_list), media_type="text/event-stream") 24 | 25 | 26 | """ # Enable if needed for database queries 27 | from src.startup.database import run_query 28 | 29 | @app.get("/db_test/") 30 | async def test_db(): 31 | result = await run_query('SELECT id FROM stg.tbl WHERE id=1') 32 | return result[0] 33 | """ 34 | -------------------------------------------------------------------------------- /anthropic-fastapi/src/startup/throttle.py: -------------------------------------------------------------------------------- 1 | from slowapi import Limiter, _rate_limit_exceeded_handler 2 | from slowapi.util import get_remote_address 3 | from slowapi.errors import RateLimitExceeded 4 | from src.startup.app import app 5 | 6 | 7 | limiter = Limiter(key_func=get_remote_address) 8 | app.state.limiter = limiter 9 | app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) -------------------------------------------------------------------------------- /anthropic-fastapi/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 -------------------------------------------------------------------------------- /causal-simulation-dowhy/causal_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/causal-simulation-dowhy/causal_model.png -------------------------------------------------------------------------------- /edatk/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/automated-eda). -------------------------------------------------------------------------------- /edatk/assets/edatk_chart_multi_variable_10_13_2021_02_11_36_571742.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_chart_multi_variable_10_13_2021_02_11_36_571742.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_multi_variable_10_13_2021_02_11_35_826709.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_multi_variable_10_13_2021_02_11_35_826709.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_30_369170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_30_369170.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_31_674656.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_31_674656.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_33_068926.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_33_068926.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_34_567949.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_34_567949.png -------------------------------------------------------------------------------- /edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_34_972858.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/edatk/assets/edatk_charts_single_variable_10_13_2021_02_11_34_972858.png -------------------------------------------------------------------------------- /edatk/demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Automated Exploratory Data Analysis" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "# Import library\n", 17 | "import edatk as eda\n", 18 | "\n", 19 | "# Load in your dataframe (using seaborn below as an example)\n", 20 | "import seaborn as sns\n", 21 | "df = sns.load_dataset('iris')\n", 22 | "\n", 23 | "# Run auto eda, optionally pass in path for saving html report and target column\n", 24 | "eda.auto_eda(df, save_path='C:\\\\Users\\\\username\\\\Documents\\\\edatk', target_column='species')" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [] 33 | } 34 | ], 35 | "metadata": { 36 | "interpreter": { 37 | "hash": "557bea64a4d33a54b4028138dfa5f7ef28b1855e4803cd7cef7b3f2a14105b37" 38 | }, 39 | "kernelspec": { 40 | "display_name": "Python 3.9.6 64-bit ('sliced2': conda)", 41 | "name": "python3" 42 | }, 43 | "language_info": { 44 | "codemirror_mode": { 45 | "name": "ipython", 46 | "version": 3 47 | }, 48 | "file_extension": ".py", 49 | "mimetype": "text/x-python", 50 | "name": "python", 51 | "nbconvert_exporter": "python", 52 | "pygments_lexer": "ipython3", 53 | "version": "3.9.6" 54 | }, 55 | "orig_nbformat": 4 56 | }, 57 | "nbformat": 4, 58 | "nbformat_minor": 2 59 | } 60 | -------------------------------------------------------------------------------- /edatk/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | EDA Toolkit 8 | 9 | 10 | 11 |
12 |
13 |

EDA Toolkit

14 |

Automated Exploratory Data Analysis in Python

15 | 16 |

Single Column Statistics

17 |
18 | 19 | 20 |

sepal_length

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
MetricValue
Column Namesepal_length
Data Type Groupingnumeric
Data Typefloat64
Row Count150
Distinct Count35
Missing Values0
Missing Value %0.0%
Mean5.84
Median5.8
Min4.3
Max7.9
Standard Deviation0.83
CV %14.12%
Skew0.31
Kurtosis-0.57
Text Box Plot|4.30 --||5.10 ~ 5.80 ~ 6.40||-- 7.90|
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | 124 | 125 | 126 |

sepal_width

127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 |
MetricValue
Column Namesepal_width
Data Type Groupingnumeric
Data Typefloat64
Row Count150
Distinct Count23
Missing Values0
Missing Value %0.0%
Mean3.06
Median3.0
Min2.0
Max4.4
Standard Deviation0.43
CV %14.21%
Skew0.32
Kurtosis0.18
Text Box Plot|2.00 --||2.80 ~ 3.00 ~ 3.30||-- 4.40|
221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 |
229 | 230 | 231 | 232 |

petal_length

233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 |
MetricValue
Column Namepetal_length
Data Type Groupingnumeric
Data Typefloat64
Row Count150
Distinct Count43
Missing Values0
Missing Value %0.0%
Mean3.76
Median4.35
Min1.0
Max6.9
Standard Deviation1.76
CV %46.82%
Skew-0.27
Kurtosis-1.4
Text Box Plot|1.00 --||1.60 ~ 4.35 ~ 5.10||-- 6.90|
327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 |
335 | 336 | 337 | 338 |

petal_width

339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 |
MetricValue
Column Namepetal_width
Data Type Groupingnumeric
Data Typefloat64
Row Count150
Distinct Count22
Missing Values0
Missing Value %0.0%
Mean1.2
Median1.3
Min0.1
Max2.5
Standard Deviation0.76
CV %63.34%
Skew-0.1
Kurtosis-1.34
Text Box Plot|0.10 --||0.30 ~ 1.30 ~ 1.80||-- 2.50|
433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 |
441 | 442 | 443 | 444 |

species

445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 |
MetricValue
Column Namespecies
Data Type Groupingstring
Data Typeobject
Row Count150
Distinct Count3
Missing Values0
Missing Value %0.0%
494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 |
502 | 503 | 504 |
505 | 506 |

Multi Column Statistics

507 |
508 | 509 | 510 |

Column Relationships

511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 |
519 | 520 | 521 | 522 |

Correlation Heatmap

523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 |
531 | 532 | 533 |
534 |
535 |
536 | 537 | -------------------------------------------------------------------------------- /flask-api/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/python-pbi-ml). -------------------------------------------------------------------------------- /flask-api/pbi/powerquery_code.pq: -------------------------------------------------------------------------------- 1 | let 2 | // Read in input csv 3 | Source = Csv.Document( 4 | File.Contents("D:\test_data.csv"), 5 | [ 6 | Delimiter=",", 7 | Columns=3, 8 | Encoding=1252, 9 | QuoteStyle=QuoteStyle.None 10 | ] 11 | ), 12 | 13 | // Promote headers and update types 14 | headers_promoted = Table.PromoteHeaders( 15 | Source, 16 | [PromoteAllScalars=true] 17 | ), 18 | type_changes = Table.TransformColumnTypes( 19 | headers_promoted, 20 | { 21 | {"square_footage", Int64.Type}, 22 | {"number_of_rooms", Int64.Type}, 23 | {"price", Int64.Type} 24 | } 25 | ), 26 | 27 | // Build API Parms 28 | api_url = "https://username.pythonanywhere.com/HousingClustering", 29 | content_input = Json.FromValue(type_changes), 30 | header = [ 31 | #"Content-Type"="application/json", 32 | #"accept"="application/json" 33 | ], 34 | 35 | // Make API call 36 | web_results = Json.Document( 37 | Web.Contents( 38 | api_url, 39 | [Content=content_input] 40 | ) 41 | ), 42 | 43 | // Convert api results to a table 44 | json_doc = Json.Document(web_results), 45 | table_from_list = Table.FromRecords(json_doc), 46 | update_types = Table.TransformColumnTypes( 47 | table_from_list, 48 | { 49 | {"row", Int64.Type}, 50 | {"group", Int64.Type} 51 | } 52 | ), 53 | 54 | // Join source and results 55 | idx = Table.AddIndexColumn( 56 | type_changes, 57 | "Index", // Col Name 58 | 1, // Initial 59 | 1, // Increment 60 | Int64.Type 61 | ), 62 | joined_tables = Table.Join( 63 | idx, // Table 1 64 | "Index", // Join Col 1 65 | update_types, // Table 2 66 | "row" // Join Col 2 67 | ), 68 | final_cols = Table.RemoveColumns( 69 | joined_tables, 70 | {"Index", "row"} 71 | ) 72 | in 73 | final_cols -------------------------------------------------------------------------------- /flask-api/pbi/test_data.csv: -------------------------------------------------------------------------------- 1 | square_footage,number_of_rooms,price 2 | 1065,3,250000 3 | 2700,4,300000 4 | 1550,3,250000 5 | 3000,5,350000 6 | 5000,10,750000 7 | -------------------------------------------------------------------------------- /flask-api/python_logic/API/api_operations.py: -------------------------------------------------------------------------------- 1 | # Flask Library Imports 2 | from flask import request, json 3 | from flask_restful import Resource 4 | 5 | # Local object imports 6 | from Starter.app import api 7 | from Schemas.request_schemas import request_input_schema_multiple 8 | from Schemas.response_schemas import response_schema_multiple 9 | 10 | # ML Logic 11 | from API.housing_clustering import cluster_houses 12 | 13 | 14 | def parse_request(request): 15 | """Parse json from request 16 | 17 | Args: 18 | request: incoming request from User api call 19 | 20 | Returns: 21 | dict: json dict from request object 22 | """ 23 | parsed_result = request.get_json(force=True) 24 | return parsed_result 25 | 26 | 27 | class HousingClustering(Resource): 28 | 29 | def post(self): 30 | 31 | try: 32 | # Parse request into json and validate schema 33 | json_data = parse_request(request) 34 | errors = request_input_schema_multiple.validate(json_data) 35 | 36 | # If errors exist abort and send to api caller 37 | if errors: 38 | return str(errors), 400 39 | 40 | # Run Clustering 41 | results = cluster_houses(json_data) 42 | 43 | # Dump results and ok status back to caller 44 | return response_schema_multiple.dumps(results), 200 45 | 46 | # Unknown error return 404 47 | except: 48 | return 'Unknown Error', 404 49 | 50 | 51 | api.add_resource(HousingClustering, '/HousingClustering') 52 | -------------------------------------------------------------------------------- /flask-api/python_logic/API/housing_clustering.py: -------------------------------------------------------------------------------- 1 | from sklearn.cluster import KMeans 2 | import numpy as np 3 | import pandas as pd 4 | 5 | def cluster_houses(input_data): 6 | """Cluster housing data using KMeans 7 | 8 | Args: 9 | input_data (dict): api request data containing houses to be clustered 10 | 11 | Returns: 12 | list: list of dict objects containing row and cluster group 13 | """ 14 | data = pd.DataFrame.from_dict(input_data).to_numpy() 15 | kmeans = KMeans(n_clusters=3, random_state=42).fit(data) 16 | return [{'row': (i+1), 'group': label} for i, label in enumerate(kmeans.labels_)] -------------------------------------------------------------------------------- /flask-api/python_logic/Schemas/request_schemas.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | class RequestInput(Schema): 4 | square_footage = fields.Integer(required=True, strict=True) 5 | number_of_rooms = fields.Integer(required=True, strict=True) 6 | price = fields.Integer(required=True, strict=True) 7 | 8 | # Input Schema 9 | request_input_schema_multiple = RequestInput(many=True) -------------------------------------------------------------------------------- /flask-api/python_logic/Schemas/response_schemas.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | class ClusterResultSchema(Schema): 4 | row = fields.Integer(required=True, strict=True) 5 | group = fields.Integer(required=True, strict=True) 6 | 7 | # Output Schema 8 | response_schema_multiple = ClusterResultSchema(many=True) -------------------------------------------------------------------------------- /flask-api/python_logic/SimpleRecommendation.py: -------------------------------------------------------------------------------- 1 | from Starter.app import * 2 | from API.api_operations import * -------------------------------------------------------------------------------- /flask-api/python_logic/Starter/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_restful import Api 3 | 4 | app = Flask(__name__) 5 | api = Api(app) -------------------------------------------------------------------------------- /hyperparameter-optimization/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/wine-optuna-hyperparameter). -------------------------------------------------------------------------------- /nlp/preprocess/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/nlp-preprocess). -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/customstyles.css: -------------------------------------------------------------------------------- 1 | .title-wrapper { 2 | width: 750px; 3 | display: block; 4 | height: auto; 5 | overflow: auto; 6 | } 7 | 8 | body { 9 | background-color: #000000; 10 | } 11 | 12 | h1, h2, h5 { 13 | color: #4c72b0; 14 | font-family: Franklin Gothic; 15 | padding-left: 25px; 16 | 17 | } 18 | 19 | h1, h2 { 20 | margin-top: 15px; 21 | line-height: 0.1em; 22 | } 23 | 24 | h5 { 25 | line-height: 0.0em; 26 | margin: 1.75em; 27 | } 28 | 29 | .border { 30 | border-top: 1px; 31 | border-color: rgba(154, 43, 34, 0.5); 32 | margin-left: 25px; 33 | margin-top: 25px; 34 | width: 750px; 35 | } 36 | 37 | .left-title { 38 | text-align: left; 39 | float: left; 40 | } 41 | 42 | .right-title { 43 | text-align: right; 44 | float: right; 45 | } 46 | 47 | .axis_ticks, .axis_text { 48 | color: #ae1e2f; 49 | font-size: 0.7em; 50 | font-family: Franklin Gothic; 51 | } 52 | 53 | .axis_text { 54 | fill: #ae1e2f; 55 | font-size: 0.8em; 56 | } -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/helpers/chart.js: -------------------------------------------------------------------------------- 1 | async function drawChart(text_init_header, text_lines, text_final_header, text_lines_after, reverse=false) { 2 | 3 | // Build structure object given html selector and dimensions (Optional parameter) 4 | const custom_dimension = new Dimension(1200, 400); 5 | const structure = new Structure('#wrapper', custom_dimension); 6 | 7 | // Add all lines and setup transitions 8 | for (var line_inst in text_lines){ 9 | 10 | // Add headers 11 | const initial_text_header_object = structure.svg 12 | .append('text') 13 | .attr('x', 0) 14 | .attr('y', 70 * line_inst - 2) 15 | .attr('fill','#ebeed7') 16 | .style('font-size', '1.0em') 17 | .text(text_init_header); 18 | const final_text_header_object = structure.svg 19 | .append('text') 20 | .attr('x', 0) 21 | .attr('y', 70 * line_inst - 2) 22 | .attr('fill','#4c72b0') 23 | .style('font-size', '1.0em') 24 | .attr('opacity', '0.0') 25 | .text(text_final_header); 26 | 27 | // Transition 28 | const displayTransition_i = d3.transition().delay(800*line_inst+500).duration(800); 29 | 30 | initial_text_header_object.transition(displayTransition_i) 31 | .style('opacity', '0.0'); 32 | final_text_header_object.transition(displayTransition_i) 33 | .style('opacity', '1.0'); 34 | 35 | // Loop through individual tokens 36 | var individaul_line = text_lines[line_inst]; 37 | for (var token_number in individaul_line) 38 | { 39 | // Calc color 40 | var default_color; 41 | if (reverse){ 42 | // See if one element is in another 43 | if (text_lines_after[line_inst].includes(text_lines[line_inst][token_number])){ 44 | default_color = '#ebeed7'; 45 | } else { 46 | default_color = '#ae1e2f'; 47 | } 48 | } else { 49 | // See if one element is in another 50 | if (text_lines[line_inst].includes(text_lines_after[line_inst][token_number])){ 51 | default_color = '#ebeed7'; 52 | } else { 53 | default_color = '#ae1e2f'; 54 | } 55 | } 56 | 57 | 58 | // Reverse color logic 59 | var init_color; 60 | var final_color; 61 | if (reverse){ 62 | init_color=default_color; 63 | final_color='#ebeed7'; 64 | } else { 65 | init_color='#ebeed7'; 66 | final_color=default_color; 67 | } 68 | 69 | // Add lines 70 | const initial_text_object = structure.svg 71 | .append('text') 72 | .attr('x', 175 + 135*token_number) 73 | .attr('y', 70 * line_inst) 74 | .attr('fill',init_color) 75 | .style('font-size', '1.375em') 76 | .text(text_lines[line_inst][token_number]); 77 | const final_text_object = structure.svg 78 | .append('text') 79 | .attr('x', 175 + 135*token_number) 80 | .attr('y', 70 * line_inst) 81 | .attr('fill',final_color) 82 | .style('font-size', '1.375em') 83 | .attr('opacity', '0.0') 84 | .text(text_lines_after[line_inst][token_number]); 85 | 86 | // Transitions 87 | initial_text_object.transition(displayTransition_i) 88 | .style('opacity', '0.0'); 89 | final_text_object.transition(displayTransition_i) 90 | .style('opacity', '1.0'); 91 | 92 | } 93 | } 94 | 95 | 96 | } -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/helpers/dimension.js: -------------------------------------------------------------------------------- 1 | class Dimension { 2 | /** 3 | * Builds a dimension object given required paramaters or defaults. 4 | * @param {number} width 5 | * @param {number} height 6 | * @param {Object} margin Object containing top, right, bottom, left numbers. 7 | */ 8 | constructor(width, height, margin={top: 50, right: 50, bottom: 50, left: 50}){ 9 | const window_min = d3.min([window.innerWidth, window.innerHeight]) * 0.90 10 | this.width = width || window_min; 11 | this.height = height || window_min; 12 | this.margin = margin; 13 | } 14 | 15 | /** 16 | * Getter method for the bounded width (width less margins). 17 | * @returns {number} 18 | */ 19 | get boundedWidth(){ 20 | return (this.width - this.margin.left - this.margin.right); 21 | } 22 | 23 | /** 24 | * Getter method for the bounded height (height less margins). 25 | * @returns {number} 26 | */ 27 | get boundedHeight(){ 28 | return (this.height - this.margin.top - this.margin.bottom); 29 | } 30 | } -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/helpers/structure_builders.js: -------------------------------------------------------------------------------- 1 | class Structure { 2 | /** 3 | * Builds a structure object given required paramaters or defaults. 4 | * @param {String} divName String that matches the div name in the index html file. 5 | * @param {Object} dimensions (Optional) Dimension object that has been initialized. 6 | */ 7 | constructor(divName, dimensions){ 8 | this.divName = divName; 9 | this.dimensions; 10 | if(dimensions === undefined){ 11 | this.dimensions = new Dimension(); 12 | } else { 13 | this.dimensions = dimensions; 14 | } 15 | this.svg; 16 | this.add_svg_structure(); 17 | } 18 | 19 | /** 20 | * Adds the svg structure to the main div. 21 | */ 22 | add_svg_structure (){ 23 | const wrapper = d3.select(this.divName); 24 | const svg = wrapper 25 | .append('svg') 26 | .attr('width', this.dimensions.width) 27 | .attr('height', this.dimensions.height) 28 | .append('g') 29 | .style('transform', `translate(${this.dimensions.margin.left}px, ${this.dimensions.margin.top}px)`); 30 | 31 | this.svg = svg; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample Chart 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Text Preprocessing

13 |
14 | 15 | 16 |
17 | 18 | 19 |
20 |
21 |
Notes
22 |
[1] White text shows words not affected by the preprocessing transformation.
23 |
[2] Red text shows which words were affected by the transformation.
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/lemmatize.js: -------------------------------------------------------------------------------- 1 | // Define strings 2 | const text_init_header = 'Before Lemmatization'; 3 | const text_lines = [ 4 | ['broom', 'drearily', 'sweeping'], 5 | ['broken', 'pieces', 'yesterdays', 'life'], 6 | ['somewhere', 'queen', 'weeping'], 7 | ['somewhere', 'king', 'wife'], 8 | ['wind', 'cries', 'mary'] 9 | ]; 10 | const text_final_header = 'After Lemmatization'; 11 | const text_lines_after = [ 12 | ['broom', 'drearily', 'sweep'], 13 | ['break', 'piece', 'yesterday', 'life'], 14 | ['somewhere', 'queen', 'weep'], 15 | ['somewhere', 'king', 'wife'], 16 | ['wind', 'cry', 'mary'] 17 | ]; 18 | 19 | drawChart(text_init_header, text_lines, text_final_header, text_lines_after); 20 | -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/stem.js: -------------------------------------------------------------------------------- 1 | // Define strings 2 | const text_init_header = 'Before Stemming'; 3 | const text_lines = [ 4 | ['broom', 'drearily', 'sweeping'], 5 | ['broken', 'pieces', 'yesterdays', 'life'], 6 | ['somewhere', 'queen', 'weeping'], 7 | ['somewhere', 'king', 'wife'], 8 | ['wind', 'cries', 'mary'] 9 | ]; 10 | const text_final_header = 'After Stemming'; 11 | const text_lines_after = [ 12 | ['broom', 'drearili', 'sweep'], 13 | ['broken', 'piec', 'yesterday', 'life'], 14 | ['somewher', 'queen', 'weep'], 15 | ['somewher', 'king', 'wife'], 16 | ['wind', 'cri', 'mari'] 17 | ]; 18 | 19 | drawChart(text_init_header, text_lines, text_final_header, text_lines_after); 20 | -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/stopwords.js: -------------------------------------------------------------------------------- 1 | // Define strings 2 | const text_init_header = 'With Stopwords'; 3 | const text_lines = [ 4 | ['A', 'broom', 'is', 'drearily', 'sweeping'], 5 | ['Up', 'the', 'broken', 'pieces', 'of', 'yesterdays', 'life'], 6 | ['Somewhere', 'a', 'queen', 'is', 'weeping'], 7 | ['Somewhere', 'a', 'king', 'has', 'no', 'wife'], 8 | ['And', 'the', 'wind', ',', 'it', 'cries', 'Mary'] 9 | ]; 10 | const text_final_header = 'Without Stopwords'; 11 | const text_lines_after = [ 12 | ['broom', 'drearily', 'sweeping'], 13 | ['broken', 'pieces', 'yesterdays', 'life'], 14 | ['Somewhere', 'queen', 'weeping'], 15 | ['Somewhere', 'king', 'wife'], 16 | ['wind', ',', 'cries', 'Mary'] 17 | ]; 18 | 19 | drawChart(text_init_header, text_lines, text_final_header, text_lines_after, true); 20 | -------------------------------------------------------------------------------- /nlp/preprocess/d3_animations/tokenization.js: -------------------------------------------------------------------------------- 1 | // Define strings 2 | const text_init_header = 'Before Tokenization'; 3 | const text_lines = [ 4 | ['A broom is drearily sweeping','','','','',''], 5 | ['Up the broken pieces of yesterdays life','','','','',''], 6 | ['Somewhere a queen is weeping','','','','',''], 7 | ['Somewhere a king has no wife','','','','',''], 8 | ['And the wind, it cries Mary','','','','',''] 9 | ]; 10 | const text_final_header = 'After Tokenization'; 11 | const text_lines_after = [ 12 | ['A', 'broom', 'is', 'drearily', 'sweeping'], 13 | ['Up', 'the', 'broken', 'pieces', 'of', 'yesterdays', 'life'], 14 | ['Somewhere', 'a', 'queen', 'is', 'weeping'], 15 | ['Somewhere', 'a', 'king', 'has', 'no', 'wife'], 16 | ['And', 'the', 'wind', ',', 'it', 'cries', 'Mary'] 17 | ]; 18 | 19 | drawChart(text_init_header, text_lines, text_final_header, text_lines_after); -------------------------------------------------------------------------------- /nlp/preprocess/data_source.txt: -------------------------------------------------------------------------------- 1 | https://www.kaggle.com/paultimothymooney/poetry?select=jimi-hendrix.txt -------------------------------------------------------------------------------- /nlp/preprocess/the_wind_cries_mary.txt: -------------------------------------------------------------------------------- 1 | After all the jacks are in their boxes 2 | And the clowns have all gone to bed 3 | You can hear happiness staggering on down the street 4 | Footprints dressed in red 5 | And the wind whispers Mary 6 | A broom is drearily sweeping 7 | Up the broken pieces of yesterdays life 8 | Somewhere a queen is weeping 9 | Somewhere a king has no wife 10 | And the wind, it cries Mary 11 | The traffic lights they all true blue tomorrow 12 | And shine their emptiness down on my bed 13 | The tiny island sags downstream 14 | Cause the life that lived is, is dead 15 | And the wind screams Mary 16 | Will the wind ever remember 17 | The names it has blown in the past 18 | And with his crutch, its old age, and it's wisdom 19 | It whispers no, this will be the last 20 | And the wind cries Mary 21 | -------------------------------------------------------------------------------- /outlier-detection/isolation-forest/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/python-outlier-detection). -------------------------------------------------------------------------------- /pydantic-ai-streaming/README.md: -------------------------------------------------------------------------------- 1 | # Pydantic AI Streaming Example 2 | 3 | This repository demonstrates how to implement streaming LLM responses with tool calls using Pydantic AI. It provides a practical example of building an extensible streaming solution that can be integrated into various backend/frontend workflows. 4 | 5 | ## Features 6 | 7 | - Async streaming of LLM responses 8 | - Tool call integration and handling 9 | - Event type classification (display vs. debug) 10 | - Structured message parsing 11 | - Integration with Anthropic's Claude 3 Sonnet 12 | 13 | ## Prerequisites 14 | 15 | - Python 3.12+ 16 | - An Anthropic API key 17 | 18 | ## Installation 19 | 20 | 1. Clone the repository and cd into pydantic-ai-streaming: 21 | ```bash 22 | git clone https://github.com/bstuddard/python-examples.git 23 | cd pydantic-ai-streaming 24 | ``` 25 | 26 | 2. Create a virtual environment and activate it (venv, conda, etc) 27 | 28 | 3. Install dependencies: 29 | ```bash 30 | pip install -r requirements.txt 31 | ``` 32 | 33 | 4. Create a `.env` file in the root directory with your Anthropic API key: 34 | ```env 35 | ANTHROPIC_API_KEY=your_api_key_here 36 | ``` 37 | 38 | ## Project Structure 39 | - src/ 40 | - pydantic_stream_example.py # Main Pydantic AI streaming implementation 41 | - base_stream_example.py # Base API streaming example 42 | - schemas.py # Data models and schemas 43 | - scripts/ 44 | - base_model_test.ipynb # Base model testing notebook 45 | - pydantic_ai_base_test.ipynb # Pydantic AI testing notebook 46 | - requirements.txt # Project dependencies 47 | 48 | ## Key Components 49 | 50 | The project includes two main approaches to streaming: 51 | 52 | 1. **Base API Streaming** (`base_stream_example.py`) 53 | - Direct integration with Anthropic's API 54 | - Basic event handling and streaming 55 | 56 | 2. **Pydantic AI Streaming** (`pydantic_stream_example.py`) 57 | - Enhanced streaming with structured events 58 | - Tool call integration 59 | - Event type classification (display vs. debug) 60 | - Structured message parsing 61 | 62 | ## Dependencies 63 | 64 | Key dependencies include: 65 | - pydantic-ai[logfire] 66 | - anthropic 67 | - python-dotenv 68 | 69 | See `requirements.txt` for a complete list of dependencies. 70 | 71 | ## Example Usage 72 | 73 | The project includes Jupyter notebooks in the `scripts` directory that demonstrate both approaches: 74 | 75 | - `base_model_test.ipynb`: Shows basic API streaming 76 | - `pydantic_ai_base_test.ipynb`: Demonstrates Pydantic AI integration 77 | 78 | ## Environment Setup 79 | 80 | Create a `.env` file in the root directory with: 81 | 82 | ```env 83 | ANTHROPIC_API_KEY=your_api_key_here # Required for Claude API access 84 | ``` 85 | 86 | ## Additional Resources 87 | 88 | - [Pydantic AI Documentation](https://ai.pydantic.dev/) 89 | - [Anthropic API Documentation](https://docs.anthropic.com/) 90 | - Blog post: [Streaming with Pydantic AI](https://datastud.dev/posts/pydantic-ai-streaming) 91 | 92 | -------------------------------------------------------------------------------- /pydantic-ai-streaming/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn[standard] 3 | gunicorn 4 | python-jose[cryptography] 5 | passlib[bcrypt] 6 | python-dotenv 7 | aioodbc 8 | python-multipart 9 | tenacity 10 | pandas 11 | pytz 12 | slowapi 13 | sendgrid 14 | anthropic 15 | langchain[anthropic] 16 | sentence-transformers 17 | pydantic-ai[logfire] -------------------------------------------------------------------------------- /pydantic-ai-streaming/scripts/base_model_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os, sys\n", 10 | "sys.path.append(os.path.dirname(os.path.dirname(os.getcwd())))\n", 11 | "sys.path.append(os.path.dirname(os.getcwd()))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from src.base_stream_example import anthropic_full_api_call\n", 21 | "from src.schemas import ChatInput\n", 22 | "from dotenv import load_dotenv\n", 23 | "load_dotenv()" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 3, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "chat_input = ChatInput(chat_input_list=[\n", 33 | " {\"role\": \"user\", \"message\": \"What is 2+2?\"}\n", 34 | "])" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "msg = await anthropic_full_api_call(chat_input_list=chat_input.chat_input_list, system_prompt=\"You are a helpful assistant that can answer questions and help with tasks.\")\n", 44 | "print(msg)" 45 | ] 46 | } 47 | ], 48 | "metadata": { 49 | "kernelspec": { 50 | "display_name": "langchain_testing_312", 51 | "language": "python", 52 | "name": "python3" 53 | }, 54 | "language_info": { 55 | "codemirror_mode": { 56 | "name": "ipython", 57 | "version": 3 58 | }, 59 | "file_extension": ".py", 60 | "mimetype": "text/x-python", 61 | "name": "python", 62 | "nbconvert_exporter": "python", 63 | "pygments_lexer": "ipython3", 64 | "version": "3.12.9" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 2 69 | } 70 | -------------------------------------------------------------------------------- /pydantic-ai-streaming/scripts/pydantic_ai_base_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os, sys\n", 10 | "sys.path.append(os.path.dirname(os.path.dirname(os.getcwd())))\n", 11 | "sys.path.append(os.path.dirname(os.getcwd()))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from pydantic_ai import Agent, Tool" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "### Simple Call" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 3, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "agent = Agent( \n", 37 | " 'anthropic:claude-3-5-sonnet-20241022',\n", 38 | " system_prompt='You are a helpful assistant that can answer questions and help with tasks.', \n", 39 | ")\n" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 4, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "async def main():\n", 49 | " agent_run = await agent.run(user_prompt='What is 2+2?')\n", 50 | " print(agent_run.data)\n", 51 | " return agent_run" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "agent_run = await main()" 61 | ] 62 | }, 63 | { 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "### Streaming" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 6, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "from src.pydantic_stream_example import main_stream" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 7, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "async def get_user_name() -> str:\n", 86 | " \"\"\"\n", 87 | " This function is used to retrieve the user name.\n", 88 | " \"\"\"\n", 89 | " return \"Joe\" # Note: this would be dynamically retrieved in an actual app" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 8, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "agent = Agent( \n", 99 | " 'anthropic:claude-3-5-sonnet-20241022',\n", 100 | " system_prompt=(\n", 101 | " \"You are a helpful assistant that can answer questions and help with tasks. Greet the user by name first before answering any questions.\"\n", 102 | " ),\n", 103 | " tools=[\n", 104 | " Tool(get_user_name, takes_ctx=False)\n", 105 | " ]\n", 106 | ")\n" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "async for text_response in main_stream(agent, 'What is 2+2?'):\n", 116 | " print(text_response, end='', flush=True)" 117 | ] 118 | } 119 | ], 120 | "metadata": { 121 | "kernelspec": { 122 | "display_name": "langchain_testing_312", 123 | "language": "python", 124 | "name": "python3" 125 | }, 126 | "language_info": { 127 | "codemirror_mode": { 128 | "name": "ipython", 129 | "version": 3 130 | }, 131 | "file_extension": ".py", 132 | "mimetype": "text/x-python", 133 | "name": "python", 134 | "nbconvert_exporter": "python", 135 | "pygments_lexer": "ipython3", 136 | "version": "3.12.9" 137 | } 138 | }, 139 | "nbformat": 4, 140 | "nbformat_minor": 2 141 | } 142 | -------------------------------------------------------------------------------- /pydantic-ai-streaming/src/base_stream_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import AsyncGenerator 3 | from anthropic import AsyncAnthropic 4 | 5 | ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY') 6 | 7 | 8 | def build_anthropic_message_input(chat_input_list: list) -> list: 9 | """Builds anthropic message input. 10 | 11 | Args: 12 | chat_input_list (list): List of chat inputs to send to the API. 13 | 14 | Returns: 15 | list: List of anthropic message input. 16 | """ 17 | message_input = [] 18 | for chat_instance in chat_input_list: 19 | message_input.append({ 20 | 'role': chat_instance['role'], 21 | "content": [ 22 | { 23 | "type": "text", 24 | "text": chat_instance['message'] 25 | } 26 | ] 27 | }) 28 | return message_input 29 | 30 | 31 | async def anthropic_stream_api_call(chat_input_list: list) -> AsyncGenerator[str, None]: 32 | """Streams anthropic response. 33 | 34 | Args: 35 | chat_input_list (list): List of chat inputs to send to the API. 36 | 37 | Yields: 38 | AsyncGenerator[str, None]: Stream of anthropic response. 39 | """ 40 | 41 | # Build message list 42 | message_input = build_anthropic_message_input(chat_input_list=chat_input_list) 43 | 44 | # Setup and make api call. 45 | client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY) 46 | stream = await client.messages.create( 47 | model="claude-3-5-sonnet-20241022", 48 | max_tokens=4096, 49 | temperature=0.2, 50 | system='You are a helpful assistant that can answer questions and help with tasks.', 51 | messages=message_input, 52 | stream=True 53 | ) 54 | 55 | async for event in stream: 56 | if event.type in ['message_start', 'message_delta', 'message_stop', 'content_block_start', 'content_block_stop']: 57 | pass 58 | elif event.type == 'content_block_delta': 59 | yield event.delta.text 60 | else: 61 | yield event.type 62 | 63 | 64 | async def anthropic_full_api_call(chat_input_list: list, system_prompt: str) -> str: 65 | """ 66 | Makes a non-streaming call to the Anthropic API and returns the full response. 67 | 68 | Args: 69 | chat_input_list (list): List of chat inputs containing role and message 70 | system_prompt (str): System prompt to use for the API call 71 | 72 | Returns: 73 | str: Complete response from the API 74 | """ 75 | message_input = build_anthropic_message_input(chat_input_list) 76 | 77 | client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY) 78 | message = await client.messages.create( 79 | model="claude-3-5-haiku-20241022", 80 | max_tokens=4096, 81 | temperature=0.2, 82 | system=system_prompt, 83 | messages=message_input 84 | ) 85 | 86 | # Extract and return the full response text 87 | output_text = message.content[0].text 88 | return str(output_text) -------------------------------------------------------------------------------- /pydantic-ai-streaming/src/pydantic_stream_example.py: -------------------------------------------------------------------------------- 1 | from pydantic_ai import Agent 2 | from pydantic_ai.messages import ( 3 | AgentStreamEvent, 4 | FinalResultEvent, 5 | FunctionToolCallEvent, 6 | FunctionToolResultEvent, 7 | PartDeltaEvent, 8 | PartStartEvent, 9 | TextPartDelta, 10 | ToolCallPartDelta, 11 | ToolCallPart 12 | ) 13 | from pydantic import BaseModel 14 | from enum import Enum 15 | from typing import AsyncIterator 16 | 17 | 18 | class EventType(str, Enum): 19 | DISPLAY = "display" 20 | DEBUG = "debug" 21 | 22 | 23 | class StreamEvent(BaseModel): 24 | event_type: EventType 25 | content: str 26 | 27 | def is_display(self) -> bool: 28 | return self.event_type == EventType.DISPLAY 29 | 30 | 31 | def build_display_event(content: str) -> StreamEvent: 32 | """ 33 | Build a display event from the provided content. 34 | 35 | Args: 36 | content (str): The content to include in the display event. 37 | 38 | Returns: 39 | StreamEvent: A validated event with display type and the provided content. 40 | """ 41 | return StreamEvent( 42 | event_type=EventType.DISPLAY, 43 | content=content 44 | ) 45 | 46 | 47 | def build_debug_event(event: AgentStreamEvent) -> StreamEvent: 48 | """ 49 | Build a debug event from an AgentStreamEvent. 50 | 51 | Args: 52 | event (AgentStreamEvent): The event to build the debug event from. 53 | 54 | Returns: 55 | StreamEvent: A validated event with debug type and the event details. 56 | """ 57 | return StreamEvent( 58 | event_type=EventType.DEBUG, 59 | content=f'{type(event)}: {event}' 60 | ) 61 | 62 | 63 | def parse_event(event: AgentStreamEvent) -> StreamEvent: 64 | """ 65 | Parse an AgentStreamEvent and return a StreamEvent. 66 | 67 | Args: 68 | event (AgentStreamEvent): The event to parse. 69 | 70 | Returns: 71 | StreamEvent: A validated event with display type and the provided content. 72 | """ 73 | 74 | # Content we care about printing 75 | if isinstance(event, PartStartEvent) and hasattr(event.part, 'content'): 76 | return build_display_event(event.part.content) 77 | 78 | elif isinstance(event, PartDeltaEvent) and hasattr(event, 'delta') and isinstance(event.delta, TextPartDelta): 79 | return build_display_event(event.delta.content_delta) 80 | 81 | # Debug only (tool calls/results) 82 | elif isinstance(event, PartStartEvent) and isinstance(event.part, ToolCallPart): 83 | return build_debug_event(event) 84 | elif isinstance(event, PartDeltaEvent) and isinstance(event.delta, ToolCallPartDelta): 85 | return build_debug_event(event) 86 | elif isinstance(event, FinalResultEvent): 87 | return build_debug_event(event) 88 | elif isinstance(event, FunctionToolCallEvent): 89 | return build_debug_event(event) 90 | elif isinstance(event, FunctionToolResultEvent): 91 | return build_debug_event(event) 92 | else: 93 | return StreamEvent( 94 | event_type=EventType.DEBUG, 95 | content=f'UNKNOWN {type(event)}: {event}' 96 | ) 97 | 98 | 99 | async def handle_stream_events(stream: AsyncIterator[AgentStreamEvent], debug_messages: list[str]) -> AsyncIterator[str]: 100 | """ 101 | Process stream events and handle display/debug routing. 102 | 103 | Args: 104 | stream: The event stream to process, yields AgentStreamEvent objects. 105 | debug_messages: List to collect debug messages. Modified in place. 106 | 107 | Yields: 108 | str: Content from display events 109 | """ 110 | async for event in stream: 111 | parsed_event = parse_event(event) 112 | if parsed_event.is_display(): 113 | yield parsed_event.content 114 | else: 115 | debug_messages.append(parsed_event.content) 116 | 117 | 118 | async def main_stream(agent: Agent, user_prompt: str) -> AsyncIterator[str]: 119 | """ 120 | Main function to handle the streaming of events from the agent based on the user prompt. 121 | 122 | This function manages the streaming of different types of nodes (user prompt, model request, tool calls, etc.) 123 | and processes the events generated during the stream. It yields the content of display events and collects 124 | debug messages for other types of events. 125 | 126 | Args: 127 | agent (Agent): The agent responsible for handling the user prompt and generating events. 128 | user_prompt (str): The user prompt to be processed by the agent. 129 | 130 | Yields: 131 | str: The content of display events generated during the stream. 132 | 133 | Collects: 134 | list: A list of debug messages generated during the stream and prints at the end. 135 | """ 136 | debug_messages = [] 137 | content_streamed = False 138 | async with agent.iter(user_prompt) as run: 139 | async for node in run: 140 | if Agent.is_user_prompt_node(node): 141 | debug_messages.append(f'UserPromptNode: {node.user_prompt}') 142 | elif Agent.is_model_request_node(node): 143 | debug_messages.append(f'ModelRequestNode: streaming partial request tokens') 144 | async with node.stream(run.ctx) as request_stream: 145 | async for content in handle_stream_events(request_stream, debug_messages): 146 | if content: 147 | content_streamed = True 148 | yield content 149 | elif Agent.is_call_tools_node(node): 150 | debug_messages.append(f'ToolCallNode: streaming tool calls and results (debug only)') 151 | async with node.stream(run.ctx) as tool_request_stream: 152 | async for _ in handle_stream_events(tool_request_stream, debug_messages): 153 | pass 154 | elif Agent.is_end_node(node): 155 | if content_streamed: 156 | debug_messages.append(f'end_node: {run.result.data}') 157 | else: 158 | yield run.result.data 159 | else: 160 | debug_messages.append(f'Unknown Node: {type(node)}: {node}') 161 | 162 | print('\n\n\nDebug:') 163 | print('\n'.join(debug_messages)) 164 | 165 | -------------------------------------------------------------------------------- /pydantic-ai-streaming/src/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ChatInput(BaseModel): 5 | chat_input_list: list 6 | 7 | class Config: 8 | json_schema_extra = { 9 | "example": { 10 | "chat_input_list": [ 11 | { 12 | "role": "user", 13 | "message": "What is your name?" 14 | }, 15 | { 16 | "role": "assistant", 17 | "message": "My name is Claude" 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /sarimax/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/python-arima-how-to). -------------------------------------------------------------------------------- /sarimax/data/Kansas_City_Crime__NIBRS__Summary.csv: -------------------------------------------------------------------------------- 1 | Date,Calendar Year,Fiscal Year Date/Time,Homicide Offenses,"Sex Offenses, Forcible",Assault Offenses," Sex Offenses, Non-forcible ",Kidnapping/Abduction,Robbery,Arson,Extortion/Blackmail,Burglary/Breaking and Entering,Larceny/Theft Offenses,Motor Vehicle Theft,Fraud Offenses,Counterfeiting/Forgery,Embezzlement,Stolen Property Offenses,Destruction/Damage/Vandalism,Bribery,Drug/Narcotic Offenses,Gambling Offenses,Prostitution Offenses,Pornography/Obscene Material,Weapon Law Violations,Total Crimes Against Persons,Total Crimes Against Property,Total Crimes Against Society 2 | 04/30/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,8,34,922,1,4,147,11,0,522,1185,319,121,30,12,21,399,0,299,0,11,1,45,969,2767,356 3 | 10/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,10,41,959,1,5,151,15,1,383,1103,395,106,42,21,40,454,0,235,0,10,2,27,1016,2711,274 4 | 07/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,9,35,1322,2,12,210,21,2,489,1241,395,137,40,20,24,553,0,312,0,12,0,45,1380,3132,369 5 | 10/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,7,31,918,2,4,144,13,0,576,1313,411,133,46,22,15,403,0,309,0,16,1,53,962,3076,379 6 | 11/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,6,36,820,1,1,154,24,0,531,1130,316,139,36,14,12,380,0,287,0,3,0,27,864,2736,317 7 | 10/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,33,887,1,5,161,31,1,655,1341,345,131,63,12,17,422,0,291,0,15,4,36,937,3179,346 8 | 02/28/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,1,31,649,2,5,88,12,1,330,858,276,99,21,8,15,309,0,282,0,6,0,35,688,2017,323 9 | 10/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,7,47,1051,2,8,192,16,1,561,1201,305,154,70,20,16,454,0,291,0,6,0,46,1115,2990,343 10 | 03/31/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,4,32,969,1,5,135,18,2,361,909,266,117,36,8,21,421,0,369,0,13,2,65,1011,2296,449 11 | 11/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,8,34,1067,1,8,160,18,0,464,1048,304,145,31,14,19,398,0,284,0,10,1,47,1118,2601,342 12 | 08/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,54,1194,2,10,167,17,1,393,1191,434,128,37,18,27,514,0,323,0,4,1,31,1277,2927,359 13 | 02/28/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,3,35,734,0,3,102,6,0,283,810,295,166,27,14,20,346,0,293,0,7,1,27,775,2069,328 14 | 06/30/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,16,50,1181,1,4,164,28,2,383,1151,400,115,39,21,20,468,0,262,0,12,0,36,1252,2791,310 15 | 12/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,8,29,831,3,5,128,23,0,546,1274,322,109,45,16,14,416,0,195,0,1,1,18,876,2893,215 16 | 12/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,9,42,876,2,1,145,17,0,518,1075,338,109,47,18,12,400,0,250,0,4,0,40,930,2679,294 17 | 02/28/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,9,49,946,2,6,120,16,0,321,910,312,128,29,13,35,382,0,427,0,11,1,37,1012,2266,476 18 | 05/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,4,41,1063,0,3,137,27,0,688,1402,237,135,36,13,15,469,0,244,0,39,0,40,1111,3159,323 19 | 11/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,8,33,818,1,8,137,30,0,637,1335,288,132,55,18,14,413,0,232,0,0,0,37,868,3059,269 20 | 05/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,42,1267,1,12,157,19,1,387,1080,327,129,48,21,21,423,0,273,0,7,1,38,1339,2613,319 21 | 09/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,12,40,1000,2,9,150,14,0,483,1279,383,140,43,10,18,379,0,281,0,5,0,55,1063,2899,341 22 | 08/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,53,1325,3,4,191,20,0,445,1281,382,120,34,18,19,510,0,393,0,10,1,47,1399,3020,451 23 | 05/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,6,38,1112,4,4,141,18,0,391,1056,255,148,35,17,17,415,0,270,0,20,1,52,1164,2493,343 24 | 10/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,11,42,1065,5,2,163,23,0,427,1057,335,151,39,14,19,433,0,307,0,12,2,53,1125,2661,374 25 | 12/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,15,23,974,1,1,187,21,0,399,1039,321,152,36,24,16,383,0,269,0,15,4,42,1014,2578,330 26 | 07/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,52,1244,3,8,178,23,0,410,1270,395,145,42,17,30,591,0,250,0,5,1,34,1324,3101,290 27 | 11/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,33,1034,0,4,166,21,0,458,1107,384,124,45,22,31,408,0,337,0,4,2,35,1085,2766,378 28 | 06/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,13,50,1002,2,11,172,17,0,560,1320,332,122,32,13,6,423,0,266,0,9,3,41,1078,2997,319 29 | 01/31/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,9,28,765,2,3,103,19,0,464,1069,362,133,47,8,19,402,0,300,0,14,2,38,807,2626,354 30 | 07/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,4,48,1013,1,3,162,21,0,470,1274,286,138,55,21,15,469,0,354,0,18,2,53,1069,2911,427 31 | 09/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,11,46,996,0,0,136,19,0,536,1190,306,119,51,21,11,418,0,402,0,7,4,33,1053,2807,446 32 | 09/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,16,38,1131,3,6,158,16,1,483,1135,339,131,38,12,21,473,0,309,0,8,1,44,1194,2807,362 33 | 07/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,8,36,1120,6,7,162,15,0,391,1146,356,152,38,16,18,462,0,286,0,3,1,40,1177,2756,330 34 | 12/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,38,975,1,5,162,13,0,431,1107,414,118,56,14,28,380,0,267,0,9,1,20,1033,2723,297 35 | 03/31/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,7,45,1159,4,9,145,22,0,347,1009,288,139,49,12,29,375,0,491,0,14,1,44,1224,2415,550 36 | 06/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,5,41,1032,3,2,151,21,0,598,1402,311,108,24,10,11,499,0,217,0,5,0,45,1083,3135,265 37 | 07/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,10,41,985,3,2,174,25,0,583,1401,385,143,58,8,16,473,0,260,0,15,2,48,1041,3266,325 38 | 05/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,6,36,1022,3,8,135,16,1,569,1364,350,107,43,16,18,447,0,315,0,10,0,34,1075,3066,359 39 | 08/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,9,41,966,2,1,154,21,0,558,1430,400,137,47,19,11,455,0,297,0,18,3,40,1019,3232,358 40 | 01/31/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,11,39,1100,1,10,171,21,0,429,1017,358,107,51,17,39,410,0,311,0,6,0,34,1161,2620,351 41 | 07/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,7,49,966,2,7,138,29,0,593,1331,372,141,61,13,13,456,0,181,0,6,0,36,1031,3147,223 42 | 12/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,7,30,799,1,1,155,17,0,476,974,393,134,47,16,15,335,0,243,0,2,2,22,838,2562,269 43 | 08/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,6,34,1074,3,1,152,19,0,544,1271,374,139,51,20,24,468,0,433,0,12,2,57,1118,3062,504 44 | 04/30/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,11,45,1090,3,9,155,14,0,322,1017,335,103,37,17,31,416,0,329,0,12,0,39,1158,2447,380 45 | 10/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,12,57,1154,2,10,171,21,1,439,1178,365,140,45,18,26,475,0,329,0,15,2,43,1235,2879,389 46 | 11/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,5,35,749,2,8,145,14,0,504,1030,330,100,41,19,17,376,0,272,0,3,0,31,799,2576,306 47 | 05/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,10,50,1154,1,7,173,20,2,386,1002,247,137,45,17,17,377,0,389,0,7,2,57,1222,2423,455 48 | 04/30/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,5,36,990,4,1,92,8,0,368,978,242,266,35,11,24,365,0,292,0,6,0,46,1036,2389,344 49 | 09/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,21,34,1164,0,5,158,27,1,416,1095,340,146,31,16,23,439,0,329,0,10,1,58,1224,2692,398 50 | 01/30/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,14,39,968,1,1,160,15,0,340,977,328,106,32,13,23,336,0,345,0,4,2,56,1023,2330,407 51 | 01/31/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,8,43,865,1,2,139,16,0,469,1112,373,115,71,12,39,364,0,312,0,7,0,34,918,2710,353 52 | 02/29/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,6,39,825,1,3,128,15,0,255,801,268,101,33,15,11,315,0,385,0,6,0,64,874,1942,455 53 | 03/31/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,4,42,853,5,2,108,20,0,451,1210,305,118,35,16,17,351,0,356,0,7,4,30,906,2631,397 54 | 06/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,6,50,1046,1,5,138,17,0,434,1169,371,146,69,12,26,373,0,356,0,11,1,35,1108,2755,403 55 | 06/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,3,33,1100,2,1,133,21,0,408,1113,311,170,33,13,18,394,0,331,0,5,1,43,1139,2614,380 56 | 02/28/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,3,27,682,1,7,95,14,0,404,966,254,88,35,7,15,330,0,192,0,0,0,26,720,2208,218 57 | 04/30/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,9,47,1076,1,5,109,12,1,351,970,226,124,36,16,14,427,0,369,0,17,0,57,1138,2286,443 58 | 05/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,5,40,1127,0,5,145,20,0,417,1319,308,148,46,10,21,448,0,417,0,20,0,50,1177,2882,487 59 | 08/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,41,1162,2,3,153,22,0,630,1398,342,124,39,12,16,485,0,224,0,26,0,45,1219,3221,295 60 | 08/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,9,42,1098,2,4,181,27,1,447,1197,350,152,36,20,22,423,0,324,0,13,0,40,1166,2856,377 61 | 03/31/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,5,25,895,1,8,117,20,0,436,1051,311,151,39,10,17,474,0,351,0,12,2,30,934,2626,395 62 | 01/31/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,15,48,894,3,2,123,17,0,632,1313,346,128,55,16,17,450,0,210,0,3,1,39,962,3097,253 63 | 06/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,10,42,1269,1,6,168,18,0,414,1016,345,128,31,21,22,481,0,340,0,10,0,61,1328,2644,411 64 | 09/30/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,11,51,1207,1,14,168,11,0,444,1179,378,155,37,16,25,515,0,271,0,8,0,35,1284,2928,314 65 | 03/31/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,10,32,988,3,6,91,4,0,325,907,235,218,43,15,21,392,0,368,0,18,1,49,1039,2251,436 66 | 09/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,39,1143,1,4,182,23,0,544,1280,329,101,33,13,6,381,0,232,0,15,1,35,1198,2911,283 67 | 04/30/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,6,34,999,0,0,122,26,0,423,1110,271,156,37,14,16,387,0,396,0,9,0,43,1039,2562,448 68 | -------------------------------------------------------------------------------- /sarimax/data/final_results.csv: -------------------------------------------------------------------------------- 1 | Date,Burglary/Breaking and Entering,month,prediction 2 | 2012-05-31,22.193548387096776,5, 3 | 2012-06-30,19.933333333333334,6, 4 | 2012-07-31,19.129032258064516,7, 5 | 2012-08-31,20.322580645161292,8, 6 | 2012-09-30,18.133333333333333,9, 7 | 2012-10-31,21.129032258064516,10, 8 | 2012-11-30,21.233333333333334,11, 9 | 2012-12-31,17.612903225806452,12, 10 | 2013-01-31,20.387096774193548,1, 11 | 2013-02-28,14.428571428571429,2, 12 | 2013-03-31,14.548387096774194,3, 13 | 2013-04-30,17.4,4, 14 | 2013-05-31,18.35483870967742,5, 15 | 2013-06-30,18.666666666666668,6, 16 | 2013-07-31,18.806451612903224,7, 17 | 2013-08-31,18.0,8, 18 | 2013-09-30,16.1,9, 19 | 2013-10-31,18.580645161290324,10, 20 | 2013-11-30,17.7,11, 21 | 2013-12-31,15.35483870967742,12, 22 | 2014-01-31,14.96774193548387,1, 23 | 2014-02-28,11.785714285714286,2, 24 | 2014-03-31,14.064516129032258,3, 25 | 2014-04-30,14.1,4, 26 | 2014-05-31,13.451612903225806,5, 27 | 2014-06-30,14.466666666666667,6, 28 | 2014-07-31,15.161290322580646,7, 29 | 2014-08-31,17.548387096774192,8, 30 | 2014-09-30,17.866666666666667,9, 31 | 2014-10-31,18.096774193548388,10, 32 | 2014-11-30,16.8,11, 33 | 2014-12-31,16.70967741935484,12, 34 | 2015-01-31,15.129032258064516,1, 35 | 2015-02-28,10.107142857142858,2, 36 | 2015-03-31,10.483870967741936,3, 37 | 2015-04-30,12.266666666666667,4, 38 | 2015-05-31,13.033333333333333,5, 39 | 2015-06-30,13.6,6, 40 | 2015-07-31,13.033333333333333,7, 41 | 2015-08-31,14.9,8, 42 | 2015-09-30,13.866666666666667,9, 43 | 2015-10-31,14.233333333333333,10, 44 | 2015-11-30,15.466666666666667,11, 45 | 2015-12-31,13.3,12, 46 | 2016-01-31,11.333333333333334,1, 47 | 2016-02-29,8.793103448275861,2, 48 | 2016-03-31,11.64516129032258,3, 49 | 2016-04-30,11.7,4, 50 | 2016-05-31,12.451612903225806,5, 51 | 2016-06-30,13.8,6, 52 | 2016-07-31,15.774193548387096,7, 53 | 2016-08-31,14.35483870967742,8, 54 | 2016-09-30,16.1,9, 55 | 2016-10-31,14.161290322580646,10, 56 | 2016-11-30,15.266666666666667,11,14.852633913270935 57 | 2016-12-31,13.903225806451612,12,12.928117482705618 58 | 2017-01-31,13.838709677419354,1,12.702752665835131 59 | 2017-02-28,11.464285714285714,2,8.590140855819062 60 | 2017-03-31,11.193548387096774,3,10.046374276249482 61 | 2017-04-30,10.733333333333333,4,11.287448019657544 62 | 2017-05-31,12.483870967741936,5,11.8876210828478 63 | 2017-06-30,12.766666666666667,6,12.625110362576478 64 | 2017-07-31,13.225806451612904,7,12.86640926573384 65 | 2017-08-31,12.67741935483871,8,14.186024494335689 66 | 2017-09-30,14.8,9,13.399517553281527 67 | 2017-10-31,12.35483870967742,10,14.511488179912929 68 | -------------------------------------------------------------------------------- /sarimax/data/prelim_data.csv: -------------------------------------------------------------------------------- 1 | Date,Burglary/Breaking and Entering,month,observed,residual,seasonal,trend,trend_plus_resid 2 | 2012-11-30,21.233333333333334,11,21.233333333333334,0.3466469900050635,2.1757032404096845,18.710983102918586,19.05763009292365 3 | 2012-12-31,17.612903225806452,12,17.612903225806452,-1.1707813254071242,0.28542546263190716,18.49825908858167,17.327477763174546 4 | 2013-01-31,20.387096774193548,1,20.387096774193548,1.8608494989656363,0.09420682463907559,18.432040450588836,20.29288994955447 5 | 2013-02-28,14.428571428571429,2,14.428571428571429,0.0910970623959022,-3.9843510306498695,18.321825396825396,18.4129224592213 6 | 2013-03-31,14.548387096774194,3,14.548387096774194,-1.097786253722536,-2.494155630558056,18.140328981054786,17.04254272733225 7 | 2013-04-30,17.4,4,17.4,0.6697876710086454,-1.2192116341422867,17.94942396313364,18.619211634142285 8 | 2013-05-31,18.35483870967742,5,18.35483870967742,1.2440795613218099,-0.5852597968568836,17.696018945212494,18.940098506534305 9 | 2013-06-30,18.666666666666668,6,18.666666666666668,1.0260389400553822,0.18591702512639016,17.454710701484895,18.480749641540278 10 | 2013-07-31,18.806451612903224,7,18.806451612903224,1.2108208994341143,0.46081248510249434,17.134818228366616,18.34563912780073 11 | 2013-08-31,18.0,8,18.0,-0.6128262599610963,1.8139335308264322,16.798892729134664,16.186066469173568 12 | 2013-09-30,16.1,9,16.1,-1.6294543034662263,1.0608419122731911,16.668612391193037,15.039158087726811 13 | 2013-10-31,18.580645161290324,10,18.580645161290324,-0.1364435507780537,2.2061376111979203,16.510951100870457,16.374507550092403 14 | 2013-11-30,17.7,11,17.7,-0.6448532660113235,2.1757032404096845,16.16915002560164,15.524296759590314 15 | 2013-12-31,15.35483870967742,12,15.35483870967742,-0.7204357032873087,0.28542546263190716,15.789848950332821,15.069413247045512 16 | 2014-01-31,14.96774193548387,1,14.96774193548387,-0.5894321190579183,0.09420682463907559,15.462967229902713,14.873535110844795 17 | 2014-02-28,11.785714285714286,2,11.785714285714286,0.4777970111926253,-3.9843510306498695,15.29226830517153,15.770065316364157 18 | 2014-03-31,14.064516129032258,3,14.064516129032258,1.2116095476087483,-2.494155630558056,15.347062211981566,16.558671759590315 19 | 2014-04-30,14.1,4,14.1,-0.08130039862781047,-1.2192116341422867,15.400512032770097,15.319211634142286 20 | 2014-05-31,13.451612903225806,5,13.451612903225806,-1.3059780423648257,-0.5852597968568836,15.342850742447515,14.03687270008269 21 | 2014-06-30,14.466666666666667,6,14.466666666666667,-1.081052713810465,0.18591702512639016,15.361802355350742,14.280749641540277 22 | 2014-07-31,15.161290322580646,7,15.161290322580646,-0.7244965608833422,0.46081248510249434,15.424974398361494,14.700477837478152 23 | 2014-08-31,17.548387096774192,8,17.548387096774192,0.37269921366921466,1.8139335308264322,15.361754352278545,15.73445356594776 24 | 2014-09-30,17.866666666666667,9,17.866666666666667,1.6632044266925048,1.0608419122731911,15.142620327700971,16.805824754393477 25 | 2014-10-31,18.096774193548388,10,18.096774193548388,0.97359869192548,2.2061376111979203,14.917037890424988,15.890636582350467 26 | 2014-11-30,16.8,11,16.8,-0.19892392653359625,2.1757032404096845,14.823220686123912,14.624296759590315 27 | 2014-12-31,16.70967741935484,12,16.70967741935484,1.6545706971223177,0.28542546263190716,14.769681259600615,16.424251956722934 28 | 2015-01-31,15.129032258064516,1,15.129032258064516,0.3899201594879086,0.09420682463907559,14.644905273937532,15.034825433425441 29 | 2015-02-28,10.107142857142858,2,10.107142857142858,-0.3543970492272406,-3.9843510306498695,14.445890937019968,14.091493887792726 30 | 2015-03-31,10.483870967741936,3,10.483870967741936,-1.1908482096877173,-2.494155630558056,14.16887480798771,12.978026598299993 31 | 2015-04-30,12.266666666666667,4,12.266666666666667,-0.35535313800312984,-1.2192116341422867,13.841231438812084,13.485878300808954 32 | 2015-05-31,13.033333333333333,5,13.033333333333333,-0.006106050557349141,-0.5852597968568836,13.624699180747566,13.618593130190217 33 | 2015-06-30,13.6,6,13.6,-0.012990757845282336,0.18591702512639016,13.427073732718892,13.41408297487361 34 | 2015-07-31,13.033333333333333,7,13.033333333333333,-0.5543288701511373,0.46081248510249434,13.126849718381976,12.57252084823084 35 | 2015-08-31,14.9,8,14.9,0.17212251469151596,1.8139335308264322,12.913943954482052,13.086066469173568 36 | 2015-09-30,13.866666666666667,9,13.866666666666667,-0.10175465482664325,1.0608419122731911,12.90757940922012,12.805824754393477 37 | 2015-10-31,14.233333333333333,10,14.233333333333333,-0.9051596727477906,2.2061376111979203,12.932355394883203,12.027195722135412 38 | 2015-11-30,15.466666666666667,11,15.466666666666667,0.4064574937393699,2.1757032404096845,12.884505932517612,13.290963426256983 39 | 2015-12-31,13.3,12,13.3,0.1459736227716285,0.28542546263190716,12.868600914596465,13.014574537368093 40 | 2016-01-31,11.333333333333334,1,11.333333333333334,-1.7520102481961133,0.09420682463907559,12.991136756890372,11.239126508694259 41 | 2016-02-29,8.793103448275861,2,8.793103448275861,-0.3051697331617733,-3.9843510306498695,13.082624212087504,12.77745447892573 42 | 2016-03-31,11.64516129032258,3,11.64516129032258,0.9863522070010187,-2.494155630558056,13.152964713879618,14.139316920880637 43 | 2016-04-30,11.7,4,11.7,-0.32380684317819153,-1.2192116341422867,13.243018477320478,12.919211634142286 44 | -------------------------------------------------------------------------------- /sarimax/pbi/results.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/sarimax/pbi/results.pbix -------------------------------------------------------------------------------- /sarimax/python-logic/helpers.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=invalid-name 2 | import pandas as pd 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | def set_date_index(input_df, col_name='Date'): 7 | """Given a pandas df, parse and set date column to index. 8 | col_name will be removed and set as datetime index. 9 | 10 | Args: 11 | input_df (pandas dataframe): Original pandas dataframe 12 | col_name (string): Name of date column 13 | 14 | Returns: 15 | pandas dataframe: modified and sorted dataframe 16 | """ 17 | # Copy df to prevent changing original 18 | modified_df = input_df.copy() 19 | 20 | # Infer datetime from col and push to month end 21 | modified_df[col_name] = pd.to_datetime(modified_df[col_name]) \ 22 | + pd.tseries.offsets.MonthEnd(0) 23 | 24 | # Grab month and include as a new column 25 | modified_df['month'] = modified_df[col_name].dt.month 26 | 27 | # Sort and set index 28 | modified_df.sort_values(col_name, inplace=True) 29 | modified_df.set_index(col_name, inplace=True) 30 | 31 | # Set as monthly index 32 | modified_df = modified_df.asfreq('M') 33 | 34 | return modified_df 35 | 36 | 37 | def get_full_date_range(input_df, col_name='Date'): 38 | """Get a dataframe with fully padded dates. 39 | 40 | Args: 41 | input_df (pandas dataframe): 42 | col_name (str, optional): Defaults to 'Date'. 43 | 44 | Returns: 45 | pandas dataframe: list of dates from start to end by day 46 | """ 47 | first_date = np.min(pd.to_datetime(input_df[col_name])) 48 | last_date = np.max(pd.to_datetime(input_df[col_name])) 49 | datepad = pd.DataFrame( 50 | pd.date_range( 51 | first_date, 52 | last_date, 53 | freq='D' 54 | ) 55 | ) 56 | datepad.columns = ['Date'] 57 | datepad.sort_values('Date', inplace=True) 58 | datepad.set_index('Date', inplace=True) 59 | return datepad 60 | 61 | 62 | def combine_seasonal_cols(input_df, seasonal_model_results): 63 | """Adds inplace new seasonal cols to df given seasonal results 64 | 65 | Args: 66 | input_df (pandas dataframe) 67 | seasonal_model_results (statsmodels DecomposeResult object) 68 | """ 69 | # Add results to original df 70 | input_df['observed'] = seasonal_model_results.observed 71 | input_df['residual'] = seasonal_model_results.resid 72 | input_df['seasonal'] = seasonal_model_results.seasonal 73 | input_df['trend'] = seasonal_model_results.trend 74 | 75 | # NaN to Zero 76 | cols = ['observed'] 77 | for col in cols: 78 | input_df[col] = input_df[col].replace(np.nan, 0) 79 | 80 | 81 | def mround(x, m=5): 82 | '''Helper method for multiple round''' 83 | return int(m * round(float(x)/m)) 84 | 85 | 86 | def plot_components(df): 87 | """Plot data for initial visualization, ultimately visualized in Power BI 88 | 89 | Args: 90 | df (pandas dataframe) 91 | """ 92 | df_axis = df.fillna(0) 93 | ymin = mround(np.min([df_axis.observed, df_axis.trend, df_axis.seasonal, df_axis.residual]),5) 94 | ymax = mround(np.max([df_axis.observed, df_axis.trend, df_axis.seasonal, df_axis.residual]),5) 95 | ymin -= 5 96 | ymax += 5 97 | 98 | plt.figure(figsize=(10,10)) 99 | 100 | plt.subplot(4,1,1) 101 | plt.title("Original Data") 102 | plt.ylim(ymin, ymax) 103 | plt.plot(df.index, df.observed) 104 | 105 | plt.subplot(4,1,2) 106 | plt.title("Trend") 107 | plt.ylim(ymin, ymax) 108 | plt.plot(df.index, df.trend) 109 | 110 | plt.subplot(4,1,3) 111 | plt.title("Seasonal") 112 | plt.ylim(ymin, ymax) 113 | plt.plot(df.index, df.seasonal) 114 | 115 | plt.subplot(4,1,4) 116 | plt.title("Residual") 117 | plt.ylim(ymin, ymax) 118 | plt.plot(df.index, df.residual) 119 | 120 | plt.tight_layout(pad=1.0, w_pad=1.0, h_pad=1.0) 121 | 122 | 123 | def plot_results(df): 124 | """Plot data for forecast visualization 125 | 126 | Args: 127 | df (pandas dataframe) 128 | """ 129 | df_axis = df.fillna(0) 130 | ymin = mround(np.min([df_axis['Burglary/Breaking and Entering'], df_axis['prediction']]),5) 131 | ymax = mround(np.max([df_axis['Burglary/Breaking and Entering'], df_axis['prediction']]),5) 132 | ymin -= 5 133 | ymax += 5 134 | 135 | plt.figure(figsize=(10,10)) 136 | 137 | plt.subplot(1,1,1) 138 | plt.title("Original Data with Forecast") 139 | plt.ylim(ymin, ymax) 140 | plt.plot(df.index, df['Burglary/Breaking and Entering']) 141 | plt.plot(df.index, df['prediction']) 142 | 143 | plt.tight_layout(pad=1.0, w_pad=1.0, h_pad=1.0) 144 | -------------------------------------------------------------------------------- /sarimax/python-logic/seasonal_decomp_and_arima.py: -------------------------------------------------------------------------------- 1 | # Libraries 2 | import pandas as pd 3 | import numpy as np 4 | from statsmodels.tsa.seasonal import seasonal_decompose 5 | from statsmodels.tsa.statespace.sarimax import SARIMAX 6 | 7 | # Local Imports 8 | from helpers import set_date_index, combine_seasonal_cols, plot_components, plot_results 9 | 10 | 11 | # Data Load: Read in, adjust for days in month, set date index 12 | df = pd.read_csv("../data/Kansas_City_Crime__NIBRS__Summary.csv") 13 | df = df[['Date', 'Burglary/Breaking and Entering']] 14 | df['Burglary/Breaking and Entering'] = \ 15 | df['Burglary/Breaking and Entering'] \ 16 | / pd.to_datetime(df['Date']).dt.day 17 | df = set_date_index(df, 'Date') 18 | 19 | # Train/Test Split 20 | df_train = df[:-12].copy() 21 | df_test = df[-12:].copy() 22 | 23 | # Seasonal decompose 24 | sd = seasonal_decompose(df_train['Burglary/Breaking and Entering'], period=12) 25 | combine_seasonal_cols(df_train, sd) 26 | 27 | # Plot for initial inspection 28 | plot_components(df_train) 29 | 30 | # Trend and Residual combination 31 | df_train['trend_plus_resid'] = df_train['trend'] + df_train['residual'] 32 | df_train = df_train[df_train.trend_plus_resid.notnull()] 33 | 34 | # Save results at this step 35 | df_train.to_csv("../data/prelim_data.csv") 36 | 37 | # Sarimax 38 | sm = SARIMAX(df_train.trend_plus_resid, order=(1,0,0)) # to be tuned/optimized in a future article 39 | res = sm.fit() 40 | res.summary() 41 | 42 | # Make trend forecast 43 | df_test['trend_prediction'] = res.predict(start=np.min(df_test.index), end=np.max(df_test.index)) 44 | 45 | # Add Seasonal component 46 | seasonal_prediction = df_train[-12:].reset_index()[['month', 'seasonal']] 47 | df_test = df_test.reset_index().merge(seasonal_prediction, how='left', left_on='month', right_on='month').set_index('Date') 48 | df_test['combined_prediction'] = df_test['trend_prediction'] + df_test['seasonal'] 49 | 50 | # Add to orignal df for visualization 51 | df['prediction'] = df_test['combined_prediction'] 52 | 53 | # Plot comparison 54 | plot_results(df) 55 | 56 | # Save final_results 57 | df.to_csv("../data/final_results.csv") 58 | -------------------------------------------------------------------------------- /sarimax/requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.4.2 2 | backcall==0.2.0 3 | certifi==2020.12.5 4 | colorama==0.4.4 5 | cycler==0.10.0 6 | decorator==4.4.2 7 | ipykernel==5.3.4 8 | ipython==7.19.0 9 | ipython-genutils==0.2.0 10 | isort==5.6.4 11 | jedi==0.17.0 12 | jupyter-client==6.1.7 13 | jupyter-core==4.7.0 14 | kiwisolver==1.3.0 15 | lazy-object-proxy==1.4.3 16 | matplotlib==3.3.2 17 | mccabe==0.6.1 18 | mkl-fft==1.2.0 19 | mkl-random==1.1.1 20 | mkl-service==2.3.0 21 | numpy==1.19.2 22 | olefile==0.46 23 | pandas==1.1.5 24 | parso==0.8.1 25 | patsy==0.5.1 26 | pickleshare==0.7.5 27 | Pillow==8.0.1 28 | pip==20.3.3 29 | prompt-toolkit==3.0.8 30 | Pygments==2.7.3 31 | pylint==2.6.0 32 | pyparsing==2.4.7 33 | python-dateutil==2.8.1 34 | pytz==2020.4 35 | pywin32==227 36 | pyzmq==20.0.0 37 | scipy==1.5.2 38 | setuptools==51.0.0.post20201207 39 | sip==4.19.13 40 | six==1.15.0 41 | statsmodels==0.12.1 42 | toml==0.10.1 43 | tornado==6.1 44 | traitlets==5.0.5 45 | wcwidth==0.2.5 46 | wheel==0.36.2 47 | wincertstore==0.2 48 | wrapt==1.11.2 49 | -------------------------------------------------------------------------------- /seasonal-decomp/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/python-seasonality-how-to). -------------------------------------------------------------------------------- /seasonal-decomp/data/Kansas_City_Crime__NIBRS__Summary.csv: -------------------------------------------------------------------------------- 1 | Date,Calendar Year,Fiscal Year Date/Time,Homicide Offenses,"Sex Offenses, Forcible",Assault Offenses," Sex Offenses, Non-forcible ",Kidnapping/Abduction,Robbery,Arson,Extortion/Blackmail,Burglary/Breaking and Entering,Larceny/Theft Offenses,Motor Vehicle Theft,Fraud Offenses,Counterfeiting/Forgery,Embezzlement,Stolen Property Offenses,Destruction/Damage/Vandalism,Bribery,Drug/Narcotic Offenses,Gambling Offenses,Prostitution Offenses,Pornography/Obscene Material,Weapon Law Violations,Total Crimes Against Persons,Total Crimes Against Property,Total Crimes Against Society 2 | 04/30/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,8,34,922,1,4,147,11,0,522,1185,319,121,30,12,21,399,0,299,0,11,1,45,969,2767,356 3 | 10/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,10,41,959,1,5,151,15,1,383,1103,395,106,42,21,40,454,0,235,0,10,2,27,1016,2711,274 4 | 07/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,9,35,1322,2,12,210,21,2,489,1241,395,137,40,20,24,553,0,312,0,12,0,45,1380,3132,369 5 | 10/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,7,31,918,2,4,144,13,0,576,1313,411,133,46,22,15,403,0,309,0,16,1,53,962,3076,379 6 | 11/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,6,36,820,1,1,154,24,0,531,1130,316,139,36,14,12,380,0,287,0,3,0,27,864,2736,317 7 | 10/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,33,887,1,5,161,31,1,655,1341,345,131,63,12,17,422,0,291,0,15,4,36,937,3179,346 8 | 02/28/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,1,31,649,2,5,88,12,1,330,858,276,99,21,8,15,309,0,282,0,6,0,35,688,2017,323 9 | 10/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,7,47,1051,2,8,192,16,1,561,1201,305,154,70,20,16,454,0,291,0,6,0,46,1115,2990,343 10 | 03/31/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,4,32,969,1,5,135,18,2,361,909,266,117,36,8,21,421,0,369,0,13,2,65,1011,2296,449 11 | 11/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,8,34,1067,1,8,160,18,0,464,1048,304,145,31,14,19,398,0,284,0,10,1,47,1118,2601,342 12 | 08/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,54,1194,2,10,167,17,1,393,1191,434,128,37,18,27,514,0,323,0,4,1,31,1277,2927,359 13 | 02/28/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,3,35,734,0,3,102,6,0,283,810,295,166,27,14,20,346,0,293,0,7,1,27,775,2069,328 14 | 06/30/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,16,50,1181,1,4,164,28,2,383,1151,400,115,39,21,20,468,0,262,0,12,0,36,1252,2791,310 15 | 12/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,8,29,831,3,5,128,23,0,546,1274,322,109,45,16,14,416,0,195,0,1,1,18,876,2893,215 16 | 12/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,9,42,876,2,1,145,17,0,518,1075,338,109,47,18,12,400,0,250,0,4,0,40,930,2679,294 17 | 02/28/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,9,49,946,2,6,120,16,0,321,910,312,128,29,13,35,382,0,427,0,11,1,37,1012,2266,476 18 | 05/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,4,41,1063,0,3,137,27,0,688,1402,237,135,36,13,15,469,0,244,0,39,0,40,1111,3159,323 19 | 11/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,8,33,818,1,8,137,30,0,637,1335,288,132,55,18,14,413,0,232,0,0,0,37,868,3059,269 20 | 05/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,42,1267,1,12,157,19,1,387,1080,327,129,48,21,21,423,0,273,0,7,1,38,1339,2613,319 21 | 09/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,12,40,1000,2,9,150,14,0,483,1279,383,140,43,10,18,379,0,281,0,5,0,55,1063,2899,341 22 | 08/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,53,1325,3,4,191,20,0,445,1281,382,120,34,18,19,510,0,393,0,10,1,47,1399,3020,451 23 | 05/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,6,38,1112,4,4,141,18,0,391,1056,255,148,35,17,17,415,0,270,0,20,1,52,1164,2493,343 24 | 10/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,11,42,1065,5,2,163,23,0,427,1057,335,151,39,14,19,433,0,307,0,12,2,53,1125,2661,374 25 | 12/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,15,23,974,1,1,187,21,0,399,1039,321,152,36,24,16,383,0,269,0,15,4,42,1014,2578,330 26 | 07/31/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,17,52,1244,3,8,178,23,0,410,1270,395,145,42,17,30,591,0,250,0,5,1,34,1324,3101,290 27 | 11/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,33,1034,0,4,166,21,0,458,1107,384,124,45,22,31,408,0,337,0,4,2,35,1085,2766,378 28 | 06/30/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,13,50,1002,2,11,172,17,0,560,1320,332,122,32,13,6,423,0,266,0,9,3,41,1078,2997,319 29 | 01/31/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,9,28,765,2,3,103,19,0,464,1069,362,133,47,8,19,402,0,300,0,14,2,38,807,2626,354 30 | 07/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,4,48,1013,1,3,162,21,0,470,1274,286,138,55,21,15,469,0,354,0,18,2,53,1069,2911,427 31 | 09/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,11,46,996,0,0,136,19,0,536,1190,306,119,51,21,11,418,0,402,0,7,4,33,1053,2807,446 32 | 09/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,16,38,1131,3,6,158,16,1,483,1135,339,131,38,12,21,473,0,309,0,8,1,44,1194,2807,362 33 | 07/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,8,36,1120,6,7,162,15,0,391,1146,356,152,38,16,18,462,0,286,0,3,1,40,1177,2756,330 34 | 12/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,14,38,975,1,5,162,13,0,431,1107,414,118,56,14,28,380,0,267,0,9,1,20,1033,2723,297 35 | 03/31/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,7,45,1159,4,9,145,22,0,347,1009,288,139,49,12,29,375,0,491,0,14,1,44,1224,2415,550 36 | 06/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,5,41,1032,3,2,151,21,0,598,1402,311,108,24,10,11,499,0,217,0,5,0,45,1083,3135,265 37 | 07/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,10,41,985,3,2,174,25,0,583,1401,385,143,58,8,16,473,0,260,0,15,2,48,1041,3266,325 38 | 05/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,6,36,1022,3,8,135,16,1,569,1364,350,107,43,16,18,447,0,315,0,10,0,34,1075,3066,359 39 | 08/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,9,41,966,2,1,154,21,0,558,1430,400,137,47,19,11,455,0,297,0,18,3,40,1019,3232,358 40 | 01/31/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,11,39,1100,1,10,171,21,0,429,1017,358,107,51,17,39,410,0,311,0,6,0,34,1161,2620,351 41 | 07/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,7,49,966,2,7,138,29,0,593,1331,372,141,61,13,13,456,0,181,0,6,0,36,1031,3147,223 42 | 12/31/2013 12:00:00 AM,2013,04/30/2014 12:00:00 AM,7,30,799,1,1,155,17,0,476,974,393,134,47,16,15,335,0,243,0,2,2,22,838,2562,269 43 | 08/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,6,34,1074,3,1,152,19,0,544,1271,374,139,51,20,24,468,0,433,0,12,2,57,1118,3062,504 44 | 04/30/2017 12:00:00 AM,2017,04/30/2017 12:00:00 AM,11,45,1090,3,9,155,14,0,322,1017,335,103,37,17,31,416,0,329,0,12,0,39,1158,2447,380 45 | 10/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,12,57,1154,2,10,171,21,1,439,1178,365,140,45,18,26,475,0,329,0,15,2,43,1235,2879,389 46 | 11/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,5,35,749,2,8,145,14,0,504,1030,330,100,41,19,17,376,0,272,0,3,0,31,799,2576,306 47 | 05/31/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,10,50,1154,1,7,173,20,2,386,1002,247,137,45,17,17,377,0,389,0,7,2,57,1222,2423,455 48 | 04/30/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,5,36,990,4,1,92,8,0,368,978,242,266,35,11,24,365,0,292,0,6,0,46,1036,2389,344 49 | 09/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,21,34,1164,0,5,158,27,1,416,1095,340,146,31,16,23,439,0,329,0,10,1,58,1224,2692,398 50 | 01/30/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,14,39,968,1,1,160,15,0,340,977,328,106,32,13,23,336,0,345,0,4,2,56,1023,2330,407 51 | 01/31/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,8,43,865,1,2,139,16,0,469,1112,373,115,71,12,39,364,0,312,0,7,0,34,918,2710,353 52 | 02/29/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,6,39,825,1,3,128,15,0,255,801,268,101,33,15,11,315,0,385,0,6,0,64,874,1942,455 53 | 03/31/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,4,42,853,5,2,108,20,0,451,1210,305,118,35,16,17,351,0,356,0,7,4,30,906,2631,397 54 | 06/30/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,6,50,1046,1,5,138,17,0,434,1169,371,146,69,12,26,373,0,356,0,11,1,35,1108,2755,403 55 | 06/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,3,33,1100,2,1,133,21,0,408,1113,311,170,33,13,18,394,0,331,0,5,1,43,1139,2614,380 56 | 02/28/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,3,27,682,1,7,95,14,0,404,966,254,88,35,7,15,330,0,192,0,0,0,26,720,2208,218 57 | 04/30/2016 12:00:00 AM,2016,04/30/2016 12:00:00 AM,9,47,1076,1,5,109,12,1,351,970,226,124,36,16,14,427,0,369,0,17,0,57,1138,2286,443 58 | 05/31/2014 12:00:00 AM,2014,04/30/2015 12:00:00 AM,5,40,1127,0,5,145,20,0,417,1319,308,148,46,10,21,448,0,417,0,20,0,50,1177,2882,487 59 | 08/31/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,41,1162,2,3,153,22,0,630,1398,342,124,39,12,16,485,0,224,0,26,0,45,1219,3221,295 60 | 08/30/2015 12:00:00 AM,2015,04/30/2016 12:00:00 AM,9,42,1098,2,4,181,27,1,447,1197,350,152,36,20,22,423,0,324,0,13,0,40,1166,2856,377 61 | 03/31/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,5,25,895,1,8,117,20,0,436,1051,311,151,39,10,17,474,0,351,0,12,2,30,934,2626,395 62 | 01/31/2013 12:00:00 AM,2013,04/30/2013 12:00:00 AM,15,48,894,3,2,123,17,0,632,1313,346,128,55,16,17,450,0,210,0,3,1,39,962,3097,253 63 | 06/30/2016 12:00:00 AM,2016,04/30/2017 12:00:00 AM,10,42,1269,1,6,168,18,0,414,1016,345,128,31,21,22,481,0,340,0,10,0,61,1328,2644,411 64 | 09/30/2017 12:00:00 AM,2017,04/30/2018 12:00:00 AM,11,51,1207,1,14,168,11,0,444,1179,378,155,37,16,25,515,0,271,0,8,0,35,1284,2928,314 65 | 03/31/2015 12:00:00 AM,2015,04/30/2015 12:00:00 AM,10,32,988,3,6,91,4,0,325,907,235,218,43,15,21,392,0,368,0,18,1,49,1039,2251,436 66 | 09/30/2012 12:00:00 AM,2012,04/30/2013 12:00:00 AM,11,39,1143,1,4,182,23,0,544,1280,329,101,33,13,6,381,0,232,0,15,1,35,1198,2911,283 67 | 04/30/2014 12:00:00 AM,2014,04/30/2014 12:00:00 AM,6,34,999,0,0,122,26,0,423,1110,271,156,37,14,16,387,0,396,0,9,0,43,1039,2562,448 68 | -------------------------------------------------------------------------------- /seasonal-decomp/data/results.csv: -------------------------------------------------------------------------------- 1 | Date,Burglary/Breaking and Entering,observed,residual,seasonal,trend 2 | 2012-05-31,22.193548387096776,22.193548387096776,,-0.677438013963929, 3 | 2012-06-30,19.933333333333334,19.933333333333334,,0.23384110789987092, 4 | 2012-07-31,19.129032258064516,19.129032258064516,,0.9011798175772899, 5 | 2012-08-31,20.322580645161292,20.322580645161292,,1.5072594066709935, 6 | 2012-09-30,18.133333333333333,18.133333333333333,,1.3556105011410395, 7 | 2012-10-31,21.129032258064516,21.129032258064516,,1.7446786014994624, 8 | 2012-11-30,21.233333333333334,21.233333333333334,0.4906938361296511,2.031656394285097,18.710983102918586 9 | 2012-12-31,17.612903225806452,17.612903225806452,-1.1404441567018937,0.25508829392667287,18.498259088581673 10 | 2013-01-31,20.387096774193548,20.387096774193548,1.8359984956278566,0.1190578279768517,18.43204045058884 11 | 2013-02-28,14.428571428571429,14.428571428571429,-0.3097957731363765,-3.583458195117591,18.321825396825396 12 | 2013-03-31,14.548387096774194,14.548387096774194,-1.1713043717556522,-2.42063751252494,18.140328981054786 13 | 2013-04-30,17.4,17.4,0.9174142662371769,-1.4668382293708182,17.94942396313364 14 | 2013-05-31,18.35483870967742,18.35483870967742,1.3362577784288554,-0.677438013963929,17.696018945212494 15 | 2013-06-30,18.666666666666668,18.666666666666668,0.9781148572819015,0.23384110789987092,17.454710701484895 16 | 2013-07-31,18.806451612903224,18.806451612903224,0.7704535669593188,0.9011798175772899,17.134818228366616 17 | 2013-08-31,18.0,18.0,-0.30615213580565404,1.5072594066709935,16.79889272913466 18 | 2013-09-30,16.1,16.1,-1.9242228923340747,1.3556105011410395,16.668612391193037 19 | 2013-10-31,18.580645161290324,18.580645161290324,0.32501545892040773,1.7446786014994624,16.510951100870454 20 | 2013-11-30,17.7,17.7,-0.5008064198867359,2.031656394285097,16.16915002560164 21 | 2013-12-31,15.35483870967742,15.35483870967742,-0.6900985345820744,0.25508829392667287,15.789848950332821 22 | 2014-01-31,14.96774193548387,14.96774193548387,-0.6142831223956962,0.1190578279768517,15.462967229902715 23 | 2014-02-28,11.785714285714286,11.785714285714286,0.07690417566034657,-3.583458195117591,15.29226830517153 24 | 2014-03-31,14.064516129032258,14.064516129032258,1.1380914295756321,-2.42063751252494,15.347062211981566 25 | 2014-04-30,14.1,14.1,0.166326196600721,-1.4668382293708182,15.400512032770097 26 | 2014-05-31,13.451612903225806,13.451612903225806,-1.2137998252577802,-0.677438013963929,15.342850742447515 27 | 2014-06-30,14.466666666666667,14.466666666666667,-1.1289767965839477,0.23384110789987092,15.361802355350743 28 | 2014-07-31,15.161290322580646,15.161290322580646,-1.1648638933581394,0.9011798175772899,15.424974398361496 29 | 2014-08-31,17.548387096774192,17.548387096774192,0.6793733378246534,1.5072594066709935,15.361754352278545 30 | 2014-09-30,17.866666666666667,17.866666666666667,1.3684358378246528,1.3556105011410395,15.142620327700975 31 | 2014-10-31,18.096774193548388,18.096774193548388,1.4350577016239396,1.7446786014994624,14.917037890424986 32 | 2014-11-30,16.8,16.8,-0.05487708040900685,2.031656394285097,14.82322068612391 33 | 2014-12-31,16.70967741935484,16.70967741935484,1.6849078658275536,0.25508829392667287,14.769681259600613 34 | 2015-01-31,15.129032258064516,15.129032258064516,0.36506915615013424,0.1190578279768517,14.64490527393753 35 | 2015-02-28,10.107142857142858,10.107142857142858,-0.7552898847595175,-3.583458195117591,14.445890937019966 36 | 2015-03-31,10.483870967741936,10.483870967741936,-1.2643663277208335,-2.42063751252494,14.16887480798771 37 | 2015-04-30,12.266666666666667,12.266666666666667,-0.10772654277459659,-1.4668382293708182,13.841231438812082 38 | 2015-05-30,13.033333333333333,13.033333333333333,0.08607216654969452,-0.677438013963929,13.624699180747568 39 | 2015-06-30,13.6,13.6,-0.060914840618763094,0.23384110789987092,13.427073732718892 40 | 2015-07-30,13.033333333333333,13.033333333333333,-0.9946962026259293,0.9011798175772899,13.126849718381973 41 | 2015-08-30,14.9,14.9,0.47879663884695645,1.5072594066709935,12.91394395448205 42 | 2015-09-30,13.866666666666667,13.866666666666667,-0.3965232436944899,1.3556105011410395,12.907579409220117 43 | 2015-10-30,14.233333333333333,14.233333333333333,-0.44370066304933276,1.7446786014994624,12.932355394883203 44 | 2015-11-30,15.466666666666667,15.466666666666667,0.5505043398639593,2.031656394285097,12.88450593251761 45 | 2015-12-30,13.3,13.3,0.17631079147686457,0.25508829392667287,12.868600914596463 46 | 2016-01-30,11.333333333333334,11.333333333333334,-1.7768612515338877,0.1190578279768517,12.99113675689037 47 | 2016-02-29,8.793103448275861,8.793103448275861,-0.7060625686940503,-3.583458195117591,13.082624212087502 48 | 2016-03-31,11.64516129032258,11.64516129032258,0.9128340889679007,-2.42063751252494,13.15296471387962 49 | 2016-04-30,11.7,11.7,-0.07618024794966005,-1.4668382293708182,13.243018477320478 50 | 2016-05-31,12.451612903225806,12.451612903225806,-0.10263243468271643,-0.677438013963929,13.231683351872451 51 | 2016-06-30,13.8,13.8,0.3176744649588622,0.23384110789987092,13.248484427141268 52 | 2016-07-31,15.774193548387096,15.774193548387096,1.4950042140628033,0.9011798175772899,13.378009516747003 53 | 2016-08-31,14.35483870967742,14.35483870967742,-0.7461201558279031,1.5072594066709935,13.59369945883433 54 | 2016-09-30,16.1,16.1,1.0582079832419644,1.3556105011410395,13.686181515616997 55 | 2016-10-31,14.161290322580646,14.161290322580646,-1.210474812456962,1.7446786014994624,13.627086533538145 56 | 2016-11-30,15.266666666666667,15.266666666666667,-0.3531425694003012,2.031656394285097,13.588152841781872 57 | 2016-12-31,13.903225806451612,13.903225806451612,0.10169614027711582,0.25508829392667287,13.546441372247823 58 | 2017-01-31,13.838709677419354,13.838709677419354,0.3224488284491591,0.1190578279768517,13.397203020993343 59 | 2017-02-28,11.464285714285714,11.464285714285714,1.8266161572271669,-3.583458195117591,13.221127752176137 60 | 2017-03-31,11.193548387096774,11.193548387096774,0.5171172872305201,-2.42063751252494,13.097068612391194 61 | 2017-04-30,10.733333333333333,10.733333333333333,-0.7674615658160759,-1.4668382293708182,12.967633128520227 62 | 2017-05-31,12.483870967741936,12.483870967741936,,-0.677438013963929, 63 | 2017-06-30,12.766666666666667,12.766666666666667,,0.23384110789987092, 64 | 2017-07-31,13.225806451612904,13.225806451612904,,0.9011798175772899, 65 | 2017-08-31,12.67741935483871,12.67741935483871,,1.5072594066709935, 66 | 2017-09-30,14.8,14.8,,1.3556105011410395, 67 | 2017-10-31,12.35483870967742,12.35483870967742,,1.7446786014994624, 68 | -------------------------------------------------------------------------------- /seasonal-decomp/pbi/results.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/seasonal-decomp/pbi/results.pbix -------------------------------------------------------------------------------- /seasonal-decomp/python-logic/helpers.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=invalid-name 2 | import pandas as pd 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | def set_date_index(input_df, col_name='Date'): 7 | """Given a pandas df, parse and set date column to index. 8 | col_name will be removed and set as datetime index. 9 | 10 | Args: 11 | input_df (pandas dataframe): Original pandas dataframe 12 | col_name (string): Name of date column 13 | 14 | Returns: 15 | pandas dataframe: modified and sorted dataframe 16 | """ 17 | # Copy df to prevent changing original 18 | modified_df = input_df.copy() 19 | 20 | # Infer datetime from col 21 | modified_df[col_name] = pd.to_datetime(modified_df[col_name]) 22 | 23 | # Sort and set index 24 | modified_df.sort_values(col_name, inplace=True) 25 | modified_df.set_index(col_name, inplace=True) 26 | 27 | return modified_df 28 | 29 | 30 | def get_full_date_range(input_df, col_name='Date'): 31 | """Get a dataframe with fully padded dates. 32 | 33 | Args: 34 | input_df (pandas dataframe): 35 | col_name (str, optional): Defaults to 'Date'. 36 | 37 | Returns: 38 | pandas dataframe: list of dates from start to end by day 39 | """ 40 | first_date = np.min(pd.to_datetime(input_df[col_name])) 41 | last_date = np.max(pd.to_datetime(input_df[col_name])) 42 | datepad = pd.DataFrame( 43 | pd.date_range( 44 | first_date, 45 | last_date, 46 | freq='D' 47 | ) 48 | ) 49 | datepad.columns = ['Date'] 50 | datepad.sort_values('Date', inplace=True) 51 | datepad.set_index('Date', inplace=True) 52 | return datepad 53 | 54 | 55 | def combine_seasonal_cols(input_df, seasonal_model_results): 56 | """Adds inplace new seasonal cols to df given seasonal results 57 | 58 | Args: 59 | input_df (pandas dataframe) 60 | seasonal_model_results (statsmodels DecomposeResult object) 61 | """ 62 | # Add results to original df 63 | input_df['observed'] = seasonal_model_results.observed 64 | input_df['residual'] = seasonal_model_results.resid 65 | input_df['seasonal'] = seasonal_model_results.seasonal 66 | input_df['trend'] = seasonal_model_results.trend 67 | 68 | # NaN to Zero 69 | cols = ['observed'] 70 | for col in cols: 71 | input_df[col] = input_df[col].replace(np.nan, 0) 72 | 73 | 74 | def mround(x, m=5): 75 | '''Helper method for multiple round''' 76 | return int(m * round(float(x)/m)) 77 | 78 | 79 | def plot_components(df): 80 | """Plot data for initial visualization, ultimately visualized in Power BI 81 | 82 | Args: 83 | df (pandas dataframe) 84 | """ 85 | df_axis = df.fillna(0) 86 | ymin = mround(np.min([df_axis.observed, df_axis.trend, df_axis.seasonal, df_axis.residual]),5) 87 | ymax = mround(np.max([df_axis.observed, df_axis.trend, df_axis.seasonal, df_axis.residual]),5) 88 | ymin -= 5 89 | ymax += 5 90 | 91 | plt.figure(figsize=(10,10)) 92 | 93 | plt.subplot(4,1,1) 94 | plt.title("Original Data") 95 | plt.ylim(ymin, ymax) 96 | plt.plot(df.index, df.observed) 97 | 98 | plt.subplot(4,1,2) 99 | plt.title("Trend") 100 | plt.ylim(ymin, ymax) 101 | plt.plot(df.index, df.trend) 102 | 103 | plt.subplot(4,1,3) 104 | plt.title("Seasonal") 105 | plt.ylim(ymin, ymax) 106 | plt.plot(df.index, df.seasonal) 107 | 108 | plt.subplot(4,1,4) 109 | plt.title("Residual") 110 | plt.ylim(ymin, ymax) 111 | plt.plot(df.index, df.residual) 112 | 113 | plt.tight_layout(pad=1.0, w_pad=1.0, h_pad=1.0) 114 | -------------------------------------------------------------------------------- /seasonal-decomp/python-logic/seasonal_decomp.py: -------------------------------------------------------------------------------- 1 | # Libraries 2 | import pandas as pd 3 | from statsmodels.tsa.seasonal import seasonal_decompose 4 | 5 | # Local Imports 6 | from helpers import set_date_index, combine_seasonal_cols, plot_components 7 | 8 | 9 | # Data Load: Read in, adjust for days in month, set date index 10 | df = pd.read_csv("../data/Kansas_City_Crime__NIBRS__Summary.csv") # https://data.kcmo.org/dataset/Kansas-City-Crime-NIBRS-Summary/6wc4-sd7p 11 | df = df[['Date', 'Burglary/Breaking and Entering']] 12 | df['Burglary/Breaking and Entering'] = \ 13 | df['Burglary/Breaking and Entering'] \ 14 | / pd.to_datetime(df['Date']).dt.day 15 | df = set_date_index(df, 'Date') 16 | 17 | # Seasonal decompose 18 | sd = seasonal_decompose(df, period=12) 19 | combine_seasonal_cols(df, sd) 20 | 21 | # Plot and output results 22 | plot_components(df) 23 | df.to_csv("../data/results.csv") 24 | -------------------------------------------------------------------------------- /seasonal-decomp/requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.4.2 2 | backcall==0.2.0 3 | certifi==2020.12.5 4 | colorama==0.4.4 5 | cycler==0.10.0 6 | decorator==4.4.2 7 | ipykernel==5.3.4 8 | ipython==7.19.0 9 | ipython-genutils==0.2.0 10 | isort==5.6.4 11 | jedi==0.17.0 12 | jupyter-client==6.1.7 13 | jupyter-core==4.7.0 14 | kiwisolver==1.3.0 15 | lazy-object-proxy==1.4.3 16 | matplotlib==3.3.2 17 | mccabe==0.6.1 18 | mkl-fft==1.2.0 19 | mkl-random==1.1.1 20 | mkl-service==2.3.0 21 | numpy==1.19.2 22 | olefile==0.46 23 | pandas==1.1.5 24 | parso==0.8.1 25 | patsy==0.5.1 26 | pickleshare==0.7.5 27 | Pillow==8.0.1 28 | pip==20.3.3 29 | prompt-toolkit==3.0.8 30 | Pygments==2.7.3 31 | pylint==2.6.0 32 | pyparsing==2.4.7 33 | python-dateutil==2.8.1 34 | pytz==2020.4 35 | pywin32==227 36 | pyzmq==20.0.0 37 | scipy==1.5.2 38 | setuptools==51.0.0.post20201207 39 | sip==4.19.13 40 | six==1.15.0 41 | statsmodels==0.12.1 42 | toml==0.10.1 43 | tornado==6.1 44 | traitlets==5.0.5 45 | wcwidth==0.2.5 46 | wheel==0.36.2 47 | wincertstore==0.2 48 | wrapt==1.11.2 49 | -------------------------------------------------------------------------------- /simple-mcp-example/README.md: -------------------------------------------------------------------------------- 1 | # Simple MCP Example 2 | 3 | This repository demonstrates how to implement a Model Context Protocol (MCP) server and client using FastAPI and Pydantic AI. It provides a practical example of building an extensible MCP solution that can be integrated into various AI workflows. 4 | 5 | ## What is MCP? 6 | 7 | The Model Context Protocol (MCP) provides a standardized way to define how an LLM interacts with tools. Instead of defining tools on a one-off basis in LLM applications, we can utilize prebuilt or custom servers that expose tools. This allows for both reusability for servers we may build ourselves or plugging into various vendor or open source MCP servers - preventing us from reinventing the wheel when we want to use a new tool. 8 | 9 | For more information, check out: 10 | - [Anthropic's release post](https://www.anthropic.com/news/model-context-protocol) 11 | - [The model context protocol site](https://modelcontextprotocol.io/introduction) 12 | - [Python SDK GitHub repo](https://github.com/modelcontextprotocol/python-sdk) 13 | 14 | ## Features 15 | 16 | - FastAPI-based MCP server implementation 17 | - Server-Sent Events (SSE) transport for streaming 18 | - Tool integration and handling 19 | - Pydantic AI integration 20 | - Simple client implementation for testing 21 | 22 | ## Prerequisites 23 | 24 | - Python 3.11+ 25 | - FastAPI and related dependencies 26 | - MCP SDK 27 | 28 | ## Installation 29 | 30 | 1. Clone the repository and cd into simple-mcp-example: 31 | ```bash 32 | git clone https://github.com/bstuddard/python-examples.git 33 | cd simple-mcp-example 34 | ``` 35 | 36 | 2. Create a virtual environment and activate it (venv, conda, etc) 37 | 38 | 3. Install dependencies: 39 | ```bash 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | ## Project Structure 44 | - src/ 45 | - build_mcp/ 46 | - create_mcp.py # MCP server creation and tool definitions 47 | - startup/ 48 | - app.py # FastAPI application setup 49 | - routes.py # API routes 50 | - mcp.py # MCP server integration with FastAPI 51 | - test_mcp/ 52 | - client.py # Simple MCP client for testing 53 | - pydantic_stream_example.py # Pydantic AI integration example 54 | - main.py # Application entry point 55 | - load_config.py # Configuration loading 56 | - requirements.txt # Project dependencies 57 | 58 | ## Key Components 59 | 60 | The project includes several key components: 61 | 62 | 1. **MCP Server** (`build_mcp/create_mcp.py`) 63 | - Tool definitions and server setup 64 | - Example tool implementation (get_user_name) 65 | - Uses FastMCP class for server creation 66 | 67 | 2. **FastAPI Integration** (`startup/mcp.py`) 68 | - SSE transport setup 69 | - MCP server mounting to FastAPI as a sub-application 70 | - Endpoint setup for SSE connections 71 | 72 | 3. **MCP Client** (`test_mcp/client.py`) 73 | - Simple client implementation for testing 74 | - Tool calling demonstration 75 | - Connection to SSE endpoint 76 | 77 | 4. **Pydantic AI Integration** (`test_mcp/pydantic_stream_example.py`) 78 | - Event handling and streaming 79 | - Structured message parsing 80 | - Integration with LLM APIs 81 | 82 | ## Dependencies 83 | 84 | Key dependencies include: 85 | - fastapi 86 | - uvicorn[standard] 87 | - mcp[cli] 88 | - pydantic-ai[logfire] 89 | - anthropic 90 | 91 | See `requirements.txt` for a complete list of dependencies. 92 | 93 | ## Usage 94 | 95 | 1. Start the FastAPI server: 96 | ```bash 97 | uvicorn src.main:app --reload --host 127.0.0.1 98 | ``` 99 | 100 | 2. In a separate terminal, run the MCP client to test: 101 | ```bash 102 | python src/test_mcp/client.py 103 | ``` 104 | 105 | 3. To use with Pydantic AI, initialize the agent with MCP servers: 106 | ```python 107 | from pydantic_ai.mcp import MCPServerHTTP 108 | 109 | # Setup mcp server objects for pydantic 110 | pydantic_mcp_server = MCPServerHTTP(url='http://127.0.0.1:8000/sse') 111 | pydantic_mcp_servers = [pydantic_mcp_server] 112 | 113 | agent = Agent( 114 | 'anthropic:claude-3-5-sonnet-20241022', 115 | system_prompt=( 116 | "You are a helpful assistant that can answer questions and help with tasks. Greet the user by name first before answering any questions." 117 | ), 118 | mcp_servers=pydantic_mcp_servers 119 | ) 120 | ``` 121 | 122 | ## Additional Resources 123 | 124 | - [MCP Documentation](https://github.com/modelcontextprotocol/spec) 125 | - [FastAPI Documentation](https://fastapi.tiangolo.com/) 126 | - [Pydantic AI Documentation](https://ai.pydantic.dev/) 127 | - [Blog Post: MCP with Pydantic AI](https://datastud.dev/posts/pydantic-ai-mcp) -------------------------------------------------------------------------------- /simple-mcp-example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn[standard] 3 | gunicorn 4 | python-jose[cryptography] 5 | passlib[bcrypt] 6 | python-dotenv 7 | aioodbc 8 | python-multipart 9 | tenacity 10 | pandas 11 | pytz 12 | slowapi 13 | sendgrid 14 | anthropic 15 | langchain[anthropic] 16 | sentence-transformers 17 | pydantic-ai[logfire] 18 | mcp[cli] -------------------------------------------------------------------------------- /simple-mcp-example/scripts/launch_uvicorn.bash: -------------------------------------------------------------------------------- 1 | uvicorn src.main:app --reload --host 127.0.0.1 -------------------------------------------------------------------------------- /simple-mcp-example/scripts/pydantic_ai_base_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 7, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os, sys\n", 10 | "sys.path.append(os.path.dirname(os.path.dirname(os.getcwd())))\n", 11 | "sys.path.append(os.path.dirname(os.getcwd()))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 8, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from pydantic_ai import Agent, Tool" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 9, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "from src.startup.mcp import pydantic_mcp_servers" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "### Streaming" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 10, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "from src.test_mcp.pydantic_stream_example import main_stream" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 11, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "agent = Agent( \n", 55 | " 'anthropic:claude-3-5-sonnet-20241022',\n", 56 | " system_prompt=(\n", 57 | " \"You are a helpful assistant that can answer questions and help with tasks. Greet the user by name first before answering any questions.\"\n", 58 | " ),\n", 59 | " mcp_servers=pydantic_mcp_servers\n", 60 | ")\n" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "async for text_response in main_stream(agent, 'What is 2+2?'):\n", 70 | " print(text_response, end='', flush=True)" 71 | ] 72 | } 73 | ], 74 | "metadata": { 75 | "kernelspec": { 76 | "display_name": "langchain_testing_312", 77 | "language": "python", 78 | "name": "python3" 79 | }, 80 | "language_info": { 81 | "codemirror_mode": { 82 | "name": "ipython", 83 | "version": 3 84 | }, 85 | "file_extension": ".py", 86 | "mimetype": "text/x-python", 87 | "name": "python", 88 | "nbconvert_exporter": "python", 89 | "pygments_lexer": "ipython3", 90 | "version": "3.12.9" 91 | } 92 | }, 93 | "nbformat": 4, 94 | "nbformat_minor": 2 95 | } 96 | -------------------------------------------------------------------------------- /simple-mcp-example/src/build_mcp/create_mcp.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | 3 | # Create an MCP server 4 | mcp = FastMCP("Demo") 5 | 6 | 7 | @mcp.tool() 8 | async def get_user_name() -> str: 9 | """ 10 | This function is used to retrieve the user name. 11 | """ 12 | return "Joe" # Note: this would be dynamically retrieved in an actual app 13 | 14 | -------------------------------------------------------------------------------- /simple-mcp-example/src/load_config.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | 3 | load_dotenv() -------------------------------------------------------------------------------- /simple-mcp-example/src/main.py: -------------------------------------------------------------------------------- 1 | from src.load_config import * 2 | from src.startup.app import * 3 | from src.startup.routes import * 4 | from src.startup.mcp import * -------------------------------------------------------------------------------- /simple-mcp-example/src/startup/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, HTTPException, status 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | # Start up app 5 | app = FastAPI() 6 | 7 | # Only only specific origins 8 | app.add_middleware( 9 | CORSMiddleware, 10 | allow_origin_regex="https?://localhost(:\d+)?|https?://127.0.0.1(:\d+)?", # Later add |^https://dev--\.netlify\.app$ 11 | allow_credentials=True, 12 | allow_methods=["GET","POST"], 13 | allow_headers=[ 14 | "Content-Type", 15 | "Authorization", 16 | "Accept", 17 | "Origin", 18 | "X-Requested-With" 19 | ], 20 | ) 21 | 22 | 23 | @app.middleware('http') 24 | async def validate_referer(request: Request, call_next): 25 | 26 | # Enable to Check Referer 27 | """ 28 | approved_referers = ['http://localhost:8000/', 'http://localhost:8080/', 'http://localhost:5173/', 'http://127.0.0.1:5173/'] # Later add , 'https://xxx.netlify.app/', 'https://xxx.azurewebsites.net/' 29 | referer = request.headers.get('referer') 30 | if referer: 31 | if not any(referer.startswith(approved_referer) for approved_referer in approved_referers): 32 | raise HTTPException( 33 | status_code=status.HTTP_400_BAD_REQUEST, 34 | detail="Not Authorized", 35 | headers={"WWW-Authenticate": "Bearer"} 36 | ) 37 | """ 38 | 39 | response = await call_next(request) 40 | return response 41 | 42 | """ # Enable if db is needed 43 | from src.startup.database import create_pool, close_pool 44 | 45 | @app.on_event("startup") 46 | async def startup(): 47 | await create_pool() 48 | 49 | 50 | @app.on_event("shutdown") 51 | async def shutdown(): 52 | await close_pool() 53 | """ -------------------------------------------------------------------------------- /simple-mcp-example/src/startup/mcp.py: -------------------------------------------------------------------------------- 1 | from src.build_mcp.create_mcp import * 2 | from src.startup.app import app 3 | from mcp.server.sse import SseServerTransport 4 | from pydantic_ai.mcp import MCPServerHTTP 5 | from starlette.routing import Mount 6 | from fastapi import Request 7 | 8 | 9 | # Mount to fastapi app 10 | sse = SseServerTransport("/messages/") 11 | app.router.routes.append(Mount("/messages", app=sse.handle_post_message)) 12 | 13 | # Add routes for sse 14 | @app.get('/sse') 15 | async def handle_sse(request: Request) -> None: 16 | # See https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/server/fastmcp/server.py for more details 17 | async with sse.connect_sse( 18 | request.scope, 19 | request.receive, 20 | request._send, # type: ignore[reportPrivateUsage] 21 | ) as streams: 22 | await mcp._mcp_server.run( 23 | streams[0], 24 | streams[1], 25 | mcp._mcp_server.create_initialization_options(), 26 | ) 27 | 28 | # Setup mcp server objects for pydantic 29 | pydantic_mcp_server = MCPServerHTTP(url='http://127.0.0.1:8000/sse') 30 | pydantic_mcp_servers = [pydantic_mcp_server] -------------------------------------------------------------------------------- /simple-mcp-example/src/startup/routes.py: -------------------------------------------------------------------------------- 1 | from src.startup.app import app 2 | 3 | 4 | 5 | @app.get("/") 6 | def read_root(): 7 | return {"Hello": "World"} 8 | -------------------------------------------------------------------------------- /simple-mcp-example/src/test_mcp/client.py: -------------------------------------------------------------------------------- 1 | from mcp import ClientSession, types 2 | from mcp.client.sse import sse_client 3 | import asyncio 4 | 5 | # This is a simple example client just utilized to test. The fast api server should already be running and this executed in a different session. 6 | 7 | async def interact_with_mcp(): 8 | print("Starting MCP client...") 9 | try: 10 | # Connect to the remote MCP server 11 | server_url = "http://127.0.0.1:8000/sse" # SSE endpoint 12 | print(f"Connecting to MCP server at {server_url}...") 13 | 14 | # First establish the SSE connection 15 | async with sse_client(server_url) as (read, write): 16 | print("Connected to SSE endpoint") 17 | async with ClientSession(read, write) as session: 18 | print("Created client session") 19 | 20 | # Initialize the connection 21 | print("Initializing connection...") 22 | await session.initialize() 23 | print("Connection initialized") 24 | 25 | # 1. List available tools 26 | print("Listing tools...") 27 | tools = await session.list_tools() 28 | print("Available tools:", tools) 29 | 30 | # 2. Call the greet tool 31 | print("Calling get name tool...") 32 | result = await session.call_tool("get_user_name") 33 | print("Get name", result) 34 | 35 | """ 36 | # 3. List available resources 37 | print("Listing resources...") 38 | resources = await session.list_resources() 39 | print("Available resources:", resources) 40 | 41 | # 4. Read the greeting resource 42 | print("Reading greeting resource...") 43 | content, mime_type = await session.read_resource("greeting://Alice") 44 | print("Greeting:", content) 45 | """ 46 | except Exception as e: 47 | print(f"Error occurred: {str(e)}") 48 | raise 49 | 50 | if __name__ == "__main__": 51 | asyncio.run(interact_with_mcp()) 52 | -------------------------------------------------------------------------------- /simple-mcp-example/src/test_mcp/pydantic_stream_example.py: -------------------------------------------------------------------------------- 1 | from pydantic_ai import Agent 2 | from pydantic_ai.messages import ( 3 | AgentStreamEvent, 4 | FinalResultEvent, 5 | FunctionToolCallEvent, 6 | FunctionToolResultEvent, 7 | PartDeltaEvent, 8 | PartStartEvent, 9 | TextPartDelta, 10 | ToolCallPartDelta, 11 | ToolCallPart 12 | ) 13 | from pydantic import BaseModel 14 | from enum import Enum 15 | from typing import AsyncIterator 16 | 17 | 18 | class EventType(str, Enum): 19 | DISPLAY = "display" 20 | DEBUG = "debug" 21 | 22 | 23 | class StreamEvent(BaseModel): 24 | event_type: EventType 25 | content: str 26 | 27 | def is_display(self) -> bool: 28 | return self.event_type == EventType.DISPLAY 29 | 30 | 31 | def build_display_event(content: str) -> StreamEvent: 32 | """ 33 | Build a display event from the provided content. 34 | 35 | Args: 36 | content (str): The content to include in the display event. 37 | 38 | Returns: 39 | StreamEvent: A validated event with display type and the provided content. 40 | """ 41 | return StreamEvent( 42 | event_type=EventType.DISPLAY, 43 | content=content 44 | ) 45 | 46 | 47 | def build_debug_event(event: AgentStreamEvent) -> StreamEvent: 48 | """ 49 | Build a debug event from an AgentStreamEvent. 50 | 51 | Args: 52 | event (AgentStreamEvent): The event to build the debug event from. 53 | 54 | Returns: 55 | StreamEvent: A validated event with debug type and the event details. 56 | """ 57 | return StreamEvent( 58 | event_type=EventType.DEBUG, 59 | content=f'{type(event)}: {event}' 60 | ) 61 | 62 | 63 | def parse_event(event: AgentStreamEvent) -> StreamEvent: 64 | """ 65 | Parse an AgentStreamEvent and return a StreamEvent. 66 | 67 | Args: 68 | event (AgentStreamEvent): The event to parse. 69 | 70 | Returns: 71 | StreamEvent: A validated event with display type and the provided content. 72 | """ 73 | 74 | # Content we care about printing 75 | if isinstance(event, PartStartEvent) and hasattr(event.part, 'content'): 76 | return build_display_event(event.part.content) 77 | 78 | elif isinstance(event, PartDeltaEvent) and hasattr(event, 'delta') and isinstance(event.delta, TextPartDelta): 79 | return build_display_event(event.delta.content_delta) 80 | 81 | # Debug only (tool calls/results) 82 | elif isinstance(event, PartStartEvent) and isinstance(event.part, ToolCallPart): 83 | return build_debug_event(event) 84 | elif isinstance(event, PartDeltaEvent) and isinstance(event.delta, ToolCallPartDelta): 85 | return build_debug_event(event) 86 | elif isinstance(event, FinalResultEvent): 87 | return build_debug_event(event) 88 | elif isinstance(event, FunctionToolCallEvent): 89 | return build_debug_event(event) 90 | elif isinstance(event, FunctionToolResultEvent): 91 | return build_debug_event(event) 92 | else: 93 | return StreamEvent( 94 | event_type=EventType.DEBUG, 95 | content=f'UNKNOWN {type(event)}: {event}' 96 | ) 97 | 98 | 99 | async def handle_stream_events(stream: AsyncIterator[AgentStreamEvent], debug_messages: list[str]) -> AsyncIterator[str]: 100 | """ 101 | Process stream events and handle display/debug routing. 102 | 103 | Args: 104 | stream: The event stream to process, yields AgentStreamEvent objects. 105 | debug_messages: List to collect debug messages. Modified in place. 106 | 107 | Yields: 108 | str: Content from display events 109 | """ 110 | async for event in stream: 111 | parsed_event = parse_event(event) 112 | if parsed_event.is_display(): 113 | yield parsed_event.content 114 | else: 115 | debug_messages.append(parsed_event.content) 116 | 117 | 118 | async def main_stream(agent: Agent, user_prompt: str) -> AsyncIterator[str]: 119 | """ 120 | Main function to handle the streaming of events from the agent based on the user prompt. 121 | 122 | This function manages the streaming of different types of nodes (user prompt, model request, tool calls, etc.) 123 | and processes the events generated during the stream. It yields the content of display events and collects 124 | debug messages for other types of events. 125 | 126 | Args: 127 | agent (Agent): The agent responsible for handling the user prompt and generating events. 128 | user_prompt (str): The user prompt to be processed by the agent. 129 | 130 | Yields: 131 | str: The content of display events generated during the stream. 132 | 133 | Collects: 134 | list: A list of debug messages generated during the stream and prints at the end. 135 | """ 136 | debug_messages = [] 137 | content_streamed = False 138 | async with agent.run_mcp_servers(): 139 | async with agent.iter(user_prompt) as run: 140 | async for node in run: 141 | if Agent.is_user_prompt_node(node): 142 | debug_messages.append(f'UserPromptNode: {node.user_prompt}') 143 | elif Agent.is_model_request_node(node): 144 | debug_messages.append(f'ModelRequestNode: streaming partial request tokens') 145 | async with node.stream(run.ctx) as request_stream: 146 | async for content in handle_stream_events(request_stream, debug_messages): 147 | if content: 148 | content_streamed = True 149 | yield content 150 | elif Agent.is_call_tools_node(node): 151 | debug_messages.append(f'ToolCallNode: streaming tool calls and results (debug only)') 152 | async with node.stream(run.ctx) as tool_request_stream: 153 | async for _ in handle_stream_events(tool_request_stream, debug_messages): 154 | pass 155 | elif Agent.is_end_node(node): 156 | if content_streamed: 157 | debug_messages.append(f'end_node: {run.result.data}') 158 | else: 159 | yield run.result.data 160 | else: 161 | debug_messages.append(f'Unknown Node: {type(node)}: {node}') 162 | 163 | print('\n\n\nDebug:') 164 | print('\n'.join(debug_messages)) 165 | 166 | -------------------------------------------------------------------------------- /sliced/2021_01/Competition.txt: -------------------------------------------------------------------------------- 1 | https://www.kaggle.com/c/sliced-s01e01 -------------------------------------------------------------------------------- /sliced/2021_01/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/sliced-2021-w1). -------------------------------------------------------------------------------- /sliced/2021_01/results.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/sliced/2021_01/results.PNG -------------------------------------------------------------------------------- /smote/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/smote). -------------------------------------------------------------------------------- /time-series-eda/data_download.py: -------------------------------------------------------------------------------- 1 | import requests, zipfile 2 | 3 | ELECTRICITY_URL = "http://archive.ics.uci.edu/ml/machine-learning-databases/00321/LD2011_2014.txt.zip" 4 | zip_save_path = './data/LD2011_2014.txt.zip' 5 | 6 | # Get file using requests library and write in chunks 7 | r = requests.get(ELECTRICITY_URL, stream=True) 8 | with open(zip_save_path, 'wb') as fd: 9 | for chunk in r.iter_content(chunk_size=128): 10 | fd.write(chunk) 11 | 12 | # Extract zip file 13 | with zipfile.ZipFile(zip_save_path, 'r') as zip_file: 14 | zip_file.extractall('./data/') -------------------------------------------------------------------------------- /time-series-resample/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | For detailed writeup, see this [blog post](https://datastud.dev/posts/time-series-resample). -------------------------------------------------------------------------------- /time-series-resample/daily_sample.csv: -------------------------------------------------------------------------------- 1 | date,value 2 | 10/1/2021,1 3 | 10/5/2021,2 4 | 10/6/2021,3 5 | 10/7/2021,4 6 | 10/10/2021,5 7 | -------------------------------------------------------------------------------- /vectorized_linear_regression/README.md: -------------------------------------------------------------------------------- 1 | ## Example and Blog Post 2 | Detailed writeup coming soon. -------------------------------------------------------------------------------- /vue-chat-frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | end_of_line = lf 9 | max_line_length = 100 10 | -------------------------------------------------------------------------------- /vue-chat-frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Anthropic Streaming + Fast API 2 | Frontend for anthropic-fastapi example -------------------------------------------------------------------------------- /vue-chat-frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vue-chat-frontend/eslint.config.ts: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue' 2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' 3 | 4 | // To allow more languages other than `ts` in `.vue` files, uncomment the following lines: 5 | // import { configureVueProject } from '@vue/eslint-config-typescript' 6 | // configureVueProject({ scriptLangs: ['ts', 'tsx'] }) 7 | // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup 8 | 9 | export default defineConfigWithVueTs( 10 | { 11 | name: 'app/files-to-lint', 12 | files: ['**/*.{ts,mts,tsx,vue}'], 13 | }, 14 | 15 | { 16 | name: 'app/files-to-ignore', 17 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 18 | }, 19 | 20 | pluginVue.configs['flat/essential'], 21 | vueTsConfigs.recommended, 22 | ) 23 | -------------------------------------------------------------------------------- /vue-chat-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Chat AI 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /vue-chat-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_streaming_frontend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build", 12 | "lint": "eslint . --fix" 13 | }, 14 | "dependencies": { 15 | "@tailwindcss/vite": "^4.0.9", 16 | "axios": "^1.8.1", 17 | "js-cookie": "^3.0.5", 18 | "localforage": "^1.10.0", 19 | "tailwindcss": "^4.0.9", 20 | "vue": "^3.5.13", 21 | "vue-router": "^4.5.0" 22 | }, 23 | "devDependencies": { 24 | "@tsconfig/node22": "^22.0.0", 25 | "@types/node": "^22.13.9", 26 | "@vitejs/plugin-vue": "^5.2.1", 27 | "@vue/eslint-config-typescript": "^14.4.0", 28 | "@vue/tsconfig": "^0.7.0", 29 | "eslint": "^9.20.1", 30 | "eslint-plugin-vue": "^9.32.0", 31 | "jiti": "^2.4.2", 32 | "npm-run-all2": "^7.0.2", 33 | "typescript": "~5.7.3", 34 | "vite": "^6.1.0", 35 | "vite-plugin-vue-devtools": "^7.7.2", 36 | "vue-tsc": "^2.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vue-chat-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstuddard/python-examples/793eb63f2c8a3d0cb533a8342bd88e2ffbe56c57/vue-chat-frontend/public/favicon.ico -------------------------------------------------------------------------------- /vue-chat-frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 23 | 24 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | html { 4 | -webkit-tap-highlight-color: transparent; 5 | } 6 | 7 | @theme { 8 | --font-sans: "Nunito Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 9 | } -------------------------------------------------------------------------------- /vue-chat-frontend/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/backgrounds/BackgroundWrapper.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/chat/ChatPageStructure.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/composables/useCache.ts: -------------------------------------------------------------------------------- 1 | // Library 2 | import { ref } from 'vue'; 3 | import Cookies from 'js-cookie'; 4 | import localForage from 'localforage'; 5 | 6 | // Types 7 | import type { Ref } from 'vue'; 8 | type StorageMode = 'memory' | 'disk' | 'diskOnly'; 9 | 10 | // Global cache object 11 | const cache: Ref> = ref({}); 12 | 13 | 14 | /** 15 | * Save a particular key and value to in memory cache. 16 | * @param key Key to save the in memory cache item to. 17 | * @param value Value to save the in memory cache item to. 18 | */ 19 | function setMemoryCache(key: string, value: any): void { 20 | cache.value[key] = value; 21 | } 22 | 23 | 24 | /** 25 | * Save a particular key and value to disk cache. 26 | * @param key Key to save disk cache to. 27 | * @param value Object to save disk cache to. 28 | */ 29 | function setDiskCache(key: string, value: any): void { 30 | localStorage.setItem(key, JSON.stringify(value)); 31 | } 32 | 33 | 34 | /** 35 | * Get a cached object from memory. 36 | * @param key Key to use to get memory cache object. 37 | * @returns Cached object. 38 | */ 39 | function getMemoryCache(key: string): any { 40 | const cachedValue: any = cache.value[key]; 41 | if (cachedValue === null){ 42 | return null; 43 | } 44 | return cachedValue; 45 | } 46 | 47 | 48 | /** 49 | * Get a cached object from disk. 50 | * @param key Key to use to get the disk cache. 51 | * @returns Cached object. 52 | */ 53 | function getDiskCache(key: string): any | null { 54 | const cachedValue: string | null = localStorage.getItem(key); 55 | if (cachedValue === null){ 56 | return null; 57 | } 58 | 59 | try { 60 | return JSON.parse(cachedValue); 61 | } catch (error){ 62 | return cachedValue; 63 | } 64 | } 65 | 66 | 67 | /** 68 | * Check if key exists in memory cache. 69 | * @param key Key to check if it exists in memory cache. 70 | * @returns true or false as to whether key exists in memory cache. 71 | */ 72 | function hasMemoryCache(key: string): boolean { 73 | return key in cache.value; 74 | } 75 | 76 | 77 | /** 78 | * Check if key exists in disk cache. 79 | * @param key Key to check if it exists in disk cache. 80 | * @returns true or false as to whether key exists in disk cache. 81 | */ 82 | function hasDiskCache(key: string): boolean { 83 | return localStorage.getItem(key) !== null; 84 | } 85 | 86 | 87 | 88 | /** 89 | * Save item to cache based on layer chosen. 90 | * @param key Key to use to set the cache. 91 | * @param value Object to save to cache. 92 | * @param storageMode memory, disk, or diskOnly. Defaults to memory. Cache layer to store as. 93 | */ 94 | function setCache(key: string, value: any, storageMode: StorageMode = 'memory'): void { 95 | if (storageMode === 'memory' || storageMode === 'disk'){ 96 | setMemoryCache(key, value); 97 | } 98 | if (storageMode === 'disk' || storageMode === 'diskOnly'){ 99 | setDiskCache(key, value); 100 | } 101 | } 102 | 103 | 104 | /** 105 | * Checks if key exists in memory cache then disk cache, returning the first available or null if nothing cached. 106 | * @param key Key to retrieve from cache. 107 | * @returns cached object. 108 | */ 109 | function getCache(key: string): any { 110 | if (hasMemoryCache(key)){ 111 | return getMemoryCache(key); 112 | } else if (hasDiskCache(key)) { 113 | return getDiskCache(key); 114 | } else { 115 | return null; 116 | } 117 | } 118 | 119 | 120 | /** 121 | * Remove all items in local storage that start with a specific prefix. 122 | * @param prefix String prefix to use to search. 123 | * @param exclude Optional string. If a key contains this string, it won't be cleared. 124 | */ 125 | function clearAllDiskCacheWithPrefix(prefix: string, exclude?: string): void { 126 | for (let i = localStorage.length - 1; i >= 0; i--) { 127 | const key = localStorage.key(i); 128 | if (key && key.startsWith(prefix) && (!exclude || !key.includes(exclude))){ 129 | localStorage.removeItem(key); 130 | } 131 | } 132 | } 133 | 134 | 135 | /** 136 | * Set cookie using js-cookie. 137 | */ 138 | function setCookie(key: string, value: string, days: number = 30): void { 139 | Cookies.set(key, value, { expires: days * 1, path: '/', secure: true, sameSite: 'Lax' }); 140 | } 141 | 142 | 143 | /** 144 | * Get cookie using js-cookie. 145 | */ 146 | function getCookie(key: string): string | undefined { 147 | return Cookies.get(key); 148 | } 149 | 150 | 151 | /** 152 | * Clear a cookie using js-cookie. 153 | */ 154 | function clearCookie(key: string): void { 155 | Cookies.remove(key, { path: '/' }); 156 | } 157 | 158 | 159 | /** 160 | * Save a particular key and value to IndexedDB cache. 161 | * @param key Key to save IndexedDB cache item to. 162 | * @param value Value to save to IndexedDB cache. 163 | */ 164 | async function setIndexedDBCache(key: string, value: any): Promise { 165 | await localForage.setItem(key, value); 166 | } 167 | 168 | /** 169 | * Get a cached object from IndexedDB. 170 | * @param key Key to use to get IndexedDB cache object. 171 | * @returns Cached object or null. 172 | */ 173 | async function getIndexedDBCache(key: string): Promise { 174 | return await localForage.getItem(key); 175 | } 176 | 177 | 178 | export default function useCache(){ 179 | return { 180 | setCache, 181 | getCache, 182 | getDiskCache, 183 | clearAllDiskCacheWithPrefix, 184 | setCookie, 185 | getCookie, 186 | clearCookie, 187 | setIndexedDBCache, 188 | getIndexedDBCache 189 | } 190 | } -------------------------------------------------------------------------------- /vue-chat-frontend/src/composables/useStreamingAPI.ts: -------------------------------------------------------------------------------- 1 | // Library 2 | import { ref } from 'vue'; 3 | 4 | // Types 5 | import type { Ref } from 'vue'; 6 | import type { ChatInputPayload } from '@/types/chatTypes'; 7 | 8 | // Local Files 9 | import { devlog } from '@/devlogger/devlogger'; 10 | 11 | 12 | 13 | // Base URL setup based on env 14 | const baseURLChoice: string = process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/stream/' : 'http://localhost:8000/stream/'; 15 | 16 | // Parsed response interface (slightly adjusted for streaming) 17 | interface StreamingParsedResponse { 18 | successful: boolean; 19 | status?: number; 20 | data: string; 21 | error?: string; 22 | } 23 | 24 | export type { StreamingParsedResponse }; 25 | export default function useStreamingAxios() { 26 | const streamData: Ref = ref(''); // Accumulated streamed response 27 | const isStreaming: Ref = ref(false); // Streaming state 28 | const streamError: Ref = ref(''); // Error message 29 | 30 | /** 31 | * Fetch a streaming response from the API 32 | * @param chatInputPayload List of ChatMessage objects 33 | */ 34 | async function fetchStream(chatInputPayload: ChatInputPayload): Promise { 35 | try { 36 | // Reset state 37 | streamData.value = ''; 38 | streamError.value = ''; 39 | isStreaming.value = true; 40 | 41 | // Build URL 42 | const url: string = baseURLChoice; 43 | 44 | // Make call and start response 45 | const response = await fetch(url, { 46 | method: 'POST', 47 | headers: { 48 | 'Accept': 'text/event-stream', 49 | 'Content-Type': 'application/json', 50 | }, 51 | body: JSON.stringify(chatInputPayload) 52 | }); 53 | if (!response.ok) { 54 | throw new Error(`HTTP error ${response.status}: ${response.statusText}`); 55 | } 56 | if (!response.body) { 57 | throw new Error('No response body available for streaming'); 58 | } 59 | 60 | // Chunk reading 61 | const reader = response.body.getReader(); 62 | const decoder = new TextDecoder(); 63 | while (true) { 64 | const { done, value } = await reader.read(); 65 | if (done) break; 66 | const chunk = decoder.decode(value, { stream: true }); 67 | streamData.value += chunk; // Append chunk reactively 68 | devlog(`Received chunk: ${chunk}`); 69 | } 70 | isStreaming.value = false; 71 | 72 | return { successful: true, data: streamData.value }; 73 | 74 | } catch (error: unknown) { 75 | let errorMessage = "An unknown error has occured." 76 | 77 | if (error instanceof Error) { 78 | errorMessage = error.message; 79 | } 80 | 81 | streamError.value = errorMessage; 82 | isStreaming.value = false; 83 | 84 | return { successful: false, data: '', error: errorMessage }; 85 | } 86 | } 87 | 88 | return { 89 | fetchStream, 90 | streamData, 91 | isStreaming, 92 | streamError 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /vue-chat-frontend/src/devlogger/devlogger.ts: -------------------------------------------------------------------------------- 1 | type LogLevel = 'info' | 'warn' | 'error'; 2 | import useCache from "@/composables/useCache"; 3 | const { setCache } = useCache() 4 | 5 | export function devlog(message: any, level: LogLevel = 'info', saveToStorage: boolean = false, storageSaveName: string | null = null): void { 6 | 7 | switch (level) { 8 | case 'error': 9 | console.error(message); 10 | break; 11 | case 'warn': 12 | console.warn(message); 13 | break; 14 | default: 15 | console.log(message); 16 | break; 17 | 18 | } 19 | 20 | // Save error message to storage if necessary. 21 | if (saveToStorage && storageSaveName) { 22 | setCache(storageSaveName, message, 'disk'); 23 | } 24 | 25 | } 26 | 27 | 28 | export function assert(condition: boolean, message?: string): void { 29 | if (!condition) { 30 | message = message || 'Assertion failed'; 31 | throw new Error(message); 32 | } 33 | } -------------------------------------------------------------------------------- /vue-chat-frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | // Library 2 | import { createApp } from 'vue'; 3 | 4 | // Types 5 | import type { App as AppInstance } from 'vue'; 6 | 7 | // Local files 8 | import App from './App.vue'; 9 | import router from './router'; 10 | import './assets/css/main.css'; 11 | 12 | 13 | // Create/route/mount app 14 | const app: AppInstance = createApp(App); 15 | app.use(router) 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | // Library 2 | import { createRouter, createWebHistory } from 'vue-router'; 3 | 4 | // Types 5 | import type { RouteRecordRaw, Router } from 'vue-router'; 6 | 7 | // Local files 8 | // import HomePage from '@/views/HomePage.vue'; 9 | import PageNotFound from '@/views/PageNotFound.vue'; 10 | import ChatPage from '@/views/ChatPage.vue'; 11 | import { devlog } from '@/devlogger/devlogger'; 12 | 13 | // Build various paths using router 14 | const routes: Array = [ 15 | { 16 | path: '/', 17 | name: 'HomePage', 18 | component: ChatPage 19 | }, 20 | { 21 | path: '/home', 22 | name: 'Home', 23 | component: ChatPage 24 | }, 25 | // Catch all for 404 Not Found 26 | { 27 | path: '/:pathMatch(.*)*', 28 | name: 'NotFound', 29 | component: PageNotFound 30 | } 31 | ] 32 | 33 | // Extend default router to capture web history 34 | const router: Router = createRouter({ 35 | history: createWebHistory(), 36 | routes 37 | }) 38 | 39 | router.beforeEach(async (to, from, next) => { 40 | devlog('Running router before.') 41 | next(); 42 | }); 43 | 44 | export default router; -------------------------------------------------------------------------------- /vue-chat-frontend/src/types/chatTypes.ts: -------------------------------------------------------------------------------- 1 | interface ChatMessage { 2 | role: string; 3 | message: string; 4 | } 5 | 6 | interface ChatInputPayload { 7 | chat_input_list: ChatMessage[]; 8 | } 9 | 10 | 11 | export type { ChatMessage, ChatInputPayload }; -------------------------------------------------------------------------------- /vue-chat-frontend/src/views/ChatPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /vue-chat-frontend/src/views/PageNotFound.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /vue-chat-frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 | 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | }, 11 | "types": ["node"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue-chat-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vue-chat-frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*", 9 | "eslint.config.*" 10 | ], 11 | "compilerOptions": { 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vue-chat-frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueDevTools from 'vite-plugin-vue-devtools' 6 | import tailwindcss from '@tailwindcss/vite' 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | vueDevTools(), 13 | tailwindcss(), 14 | ], 15 | resolve: { 16 | alias: { 17 | '@': fileURLToPath(new URL('./src', import.meta.url)) 18 | }, 19 | }, 20 | }) 21 | --------------------------------------------------------------------------------