diff --git a/price_forecast.py b/price_forecast.py new file mode 100644 index 0000000..8676097 --- /dev/null +++ b/price_forecast.py @@ -0,0 +1,400 @@ +import pandas as pd +import numpy as np +import xgboost as xgb +import mysql.connector +from mysql.connector import Error +from datetime import datetime, timedelta +import holidays + +# --- NIEUW: Imports voor grafiek en e-mail --- +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.image import MIMEImage + +# --- NIEUW: Zorgt dat matplotlib werkt zonder GUI (bv. in terminal) --- +plt.switch_backend('Agg') + +# --- CONFIGURATIE --- +MODEL_FILE = 'price_forecast_model_v1_5.json' +TARGET = 'gemiddelde_prijs' +AANTAL_UUR_VOORSPELLEN = 72 +GRAFIEK_BESTAND = 'prijs_voorspelling.png' # Tijdelijk bestand + +# --- NIEUW: E-mail Configuratie --- +# VUL DIT IN MET JE EIGEN GEGEVENS +EMAIL_CONFIG = { + 'smtp_server': '192.168.178.201', # Bv. 'smtp.gmail.com' + 'smtp_port': 587, + 'sender': 'sftpuser@markkors.nl', + 'password': 'Aae8-G9yFU5j', # Voeg hier het wachtwoord toe als dat nodig is + 'receiver': 'mark@markkors.nl' +} + +DB_CONFIG = { + 'host': '192.168.178.201', + 'user': 'energy_prices_user', + 'password': 'kS9R*xp17ZwCD@CV&E^N', + 'database': 'energy_prices', + 'port': 3307 +} + +# Configuratie voor de Alfen database met werkelijke prijzen --- +ALFEN_DB_CONFIG = { + 'host': '192.168.178.201', + 'user': 'alfen_user', + 'password': '5uVgr%f%s2P5GR@3q!', + 'database': 'alfen', + 'port': 3307 +} + +# KIES HIER DE ENERGIELEVERANCIER (of gebruik 'GEMIDDELDE' voor het gemiddelde van alle leveranciers) +LEVERANCIER = 'GEMIDDELDE' # Of bijv. 'Eneco', 'Vattenfall', etc. + +# --- Model laden --- +try: + print(f"Laden van model: {MODEL_FILE}...") + model = xgb.XGBRegressor() + model.load_model(MODEL_FILE) + FEATURES = model.feature_names_in_ + print(f"āœ… Model succesvol geladen (verwacht {len(FEATURES)} features).") +except FileNotFoundError: + print(f"āŒ Fout: Model bestand '{MODEL_FILE}' niet gevonden.") + print(" Heb je het 'v1_5' model al getraind en opgeslagen op Windows?") + exit() +except Exception as e: + print(f"āŒ Fout bij laden model: {e}") + exit() + +nl_holidays = holidays.Netherlands(years=[datetime.now().year, datetime.now().year + 1]) + + +# --- NIEUW: Functie om werkelijke prijzen op te halen uit Alfen database --- +def haal_werkelijke_prijzen(start_tijd, eind_tijd): + """ + Haalt werkelijke stroomprijzen op uit de Alfen database. + """ + print("\nšŸ’¾ Werkelijke prijzen ophalen uit Alfen database...") + + try: + conn_alfen = mysql.connector.connect(**ALFEN_DB_CONFIG) + + if LEVERANCIER == 'GEMIDDELDE': + # Haal gemiddelde prijs van alle leveranciers + query = """ + SELECT + datetime, + AVG(price) as price + FROM + dynamic_price_data_tommorow + WHERE + datetime BETWEEN %s AND %s + GROUP BY + datetime + ORDER BY + datetime + """ + else: + # Haal specifieke leverancier (voeg eventueel een WHERE clause toe voor leverancier) + # Dit hangt af van hoe je leveranciers opslaat in de tabel + query = """ + SELECT + datetime, + price + FROM + dynamic_price_data_tommorow + WHERE + datetime BETWEEN %s AND %s + ORDER BY + datetime + """ + + werkelijke_df = pd.read_sql( + query, + conn_alfen, + params=(start_tijd, eind_tijd), + index_col='datetime', + parse_dates=['datetime'] + ) + + conn_alfen.close() + + print(f"āœ… {len(werkelijke_df)} werkelijke prijzen opgehaald.") + return werkelijke_df + + except Error as e: + print(f"āš ļø Kon geen verbinding maken met Alfen database: {e}") + print(" Voorspelling gaat door zonder werkelijke prijzen in grafiek.") + return None + except Exception as e: + print(f"āš ļø Fout bij ophalen werkelijke prijzen: {e}") + return None + + +# --- Functie: haal_data_uit_database (onveranderd) --- +def haal_data_uit_database(conn): + print("šŸ’¾ Data ophalen uit MySQL...") + + query_hist = """ + SELECT + w.datum_tijd, w.temperatuur, w.gevoelstemperatuur, w.neerslag, + w.wind_richting, w.wind_snelheid, w.bewolking, w.luchtdruk, w.luchtvochtigheid, + w.zonnestraling, + p_avg.gemiddelde_prijs + FROM + amersfoort_weer_uurlijks AS w + LEFT JOIN + (SELECT datetime, AVG(price) AS gemiddelde_prijs + FROM dynamic_price_data GROUP BY datetime) AS p_avg + ON w.datum_tijd = p_avg.datetime + WHERE + w.datum_tijd BETWEEN (UTC_TIMESTAMP() - INTERVAL 30 HOUR) AND UTC_TIMESTAMP() + ORDER BY + w.datum_tijd; + """ + + query_toekomst = f""" + SELECT + datum_tijd, temperatuur, gevoelstemperatuur, neerslag, + wind_richting, wind_snelheid, bewolking, luchtdruk, luchtvochtigheid, + zonnestraling, + NULL AS gemiddelde_prijs + FROM + amersfoort_weer_uurlijks + WHERE + datum_tijd BETWEEN UTC_TIMESTAMP() AND (UTC_TIMESTAMP() + INTERVAL {AANTAL_UUR_VOORSPELLEN} HOUR) + ORDER BY + datum_tijd; + """ + + try: + hist_df = pd.read_sql(query_hist, conn, index_col='datum_tijd', parse_dates=['datum_tijd']) + toekomst_df = pd.read_sql(query_toekomst, conn, index_col='datum_tijd', parse_dates=['datum_tijd']) + + print(f"āœ… {len(hist_df)} uur historie geladen.") + print(f"āœ… {len(toekomst_df)} uur toekomstig weer geladen.") + + hist_df['gemiddelde_prijs'] = hist_df['gemiddelde_prijs'].ffill() + hist_df['gemiddelde_prijs'] = hist_df['gemiddelde_prijs'].bfill() + + combined_df = pd.concat([hist_df, toekomst_df]) + + return combined_df.sort_index() + + except Exception as e: + print(f"āŒ Fout bij ophalen data: {e}") + return None + +# --- Functie: maak_features_voor_uur (onveranderd) --- +def maak_features_voor_uur(df, timestamp): + features = {} + data_nu = df.loc[timestamp] + + features['maand'] = timestamp.month + features['dag_van_het_jaar'] = timestamp.dayofyear + features['is_feestdag'] = 1 if timestamp in nl_holidays else 0 + + weer_cols = ['temperatuur', 'gevoelstemperatuur', 'neerslag', 'wind_richting', + 'wind_snelheid', 'bewolking', 'luchtdruk', 'luchtvochtigheid', 'zonnestraling'] + for col in weer_cols: + features[col] = data_nu[col] + + features['prijs_1u_geleden'] = df.loc[timestamp - timedelta(hours=1)]['gemiddelde_prijs'] + features['prijs_24u_geleden'] = df.loc[timestamp - timedelta(hours=24)]['gemiddelde_prijs'] + + features['temp_avg_3u'] = df.loc[timestamp - timedelta(hours=2) : timestamp]['temperatuur'].mean() + features['prijs_avg_6u'] = df.loc[timestamp - timedelta(hours=5) : timestamp]['gemiddelde_prijs'].mean() + + for dag in range(7): + features[f'dag_{dag}'] = 1 if timestamp.dayofweek == dag else 0 + + for uur in range(24): + features[f'uur_{uur}'] = 1 if timestamp.hour == uur else 0 + + return pd.DataFrame([features], columns=FEATURES) + +# --- NIEUW: Functie om de e-mail te bouwen en te versturen --- +def send_email_with_graph(image_path, result_df): + print("\nšŸ“¬ E-mail opstellen...") + + msg = MIMEMultipart() + msg['Subject'] = f"Energie Prijsvoorspelling [energy_prediction] {datetime.now().strftime('%d-%m-%Y')}" + msg['From'] = EMAIL_CONFIG['sender'] + msg['To'] = EMAIL_CONFIG['receiver'] + + laagste_prijs = result_df['Voorspelde_Prijs'].min() + hoogste_prijs = result_df['Voorspelde_Prijs'].max() + laagste_moment = result_df['Voorspelde_Prijs'].idxmin() + hoogste_moment = result_df['Voorspelde_Prijs'].idxmax() + + body = f""" +Prijsvoorspelling voor de komende {len(result_df)} uur. + +Laagste prijs: {laagste_prijs:.4f} (op {laagste_moment}) +Hoogste prijs: {hoogste_prijs:.4f} (op {hoogste_moment}) + +Volledige voorspelling: +{result_df.to_string()} +""" + msg.attach(MIMEText(body, 'plain')) + + try: + with open(image_path, 'rb') as f: + img_attach = MIMEImage(f.read(), name='voorspelling.png') + msg.attach(img_attach) + print("āœ… Grafiek bijgevoegd.") + except Exception as e: + print(f"āŒ Kon grafiek-bestand niet lezen: {e}") + return + + try: + print(f"Verbinding maken met SMTP server: {EMAIL_CONFIG['smtp_server']}...") + server = smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) + server.starttls() + server.login(EMAIL_CONFIG['sender'], EMAIL_CONFIG['password']) + + print("Inloggen succesvol. E-mail verzenden...") + server.sendmail(EMAIL_CONFIG['sender'], EMAIL_CONFIG['receiver'], msg.as_string()) + server.quit() + print("āœ… E-mail succesvol verzonden.") + except Exception as e: + print(f"āŒ Fout bij verzenden e-mail: {e}") + print(" Controleer je EMAIL_CONFIG (server, poort, e-mail en App-wachtwoord)") + +# --- START VAN HET SCRIPT (Aangepast) --- +try: + conn = mysql.connector.connect(**DB_CONFIG) + werk_df = haal_data_uit_database(conn) + + if werk_df is not None: + te_voorspellen_tijden = werk_df[werk_df['gemiddelde_prijs'].isnull()].index + + if len(te_voorspellen_tijden) == 0: + print("\nDatabase is al up-to-date. Geen voorspelling nodig.") + else: + print(f"\n🧠 Start iteratieve voorspelling voor {len(te_voorspellen_tijden)} uur...") + voorspellingen = [] + + for timestamp in te_voorspellen_tijden: + features_nu = maak_features_voor_uur(werk_df, timestamp) + voorspelde_prijs = model.predict(features_nu)[0] + werk_df.loc[timestamp, 'gemiddelde_prijs'] = voorspelde_prijs + voorspellingen.append(voorspelde_prijs) + + print("\n" + "="*70) + pd.set_option('display.max_rows', None) + print(f"--- VOORSPELDE PRIJZEN (komende {len(te_voorspellen_tijden)} uur) ---") + + resultaat_df = pd.DataFrame({ + 'Voorspelde_Prijs': voorspellingen + }, index=te_voorspellen_tijden) + + print(resultaat_df) + print("="*70) + + # --- NIEUW: Haal werkelijke prijzen op --- + start_tijd = te_voorspellen_tijden[0] + eind_tijd = te_voorspellen_tijden[-1] + werkelijke_prijzen_df = haal_werkelijke_prijzen(start_tijd, eind_tijd) + + # Grafiek maken (met werkelijke prijzen) --- + print("\nšŸ“Š Grafiek genereren (met werkelijke prijzen en weer)...") + try: + # Maak de basis-figuur en de EERSTE Y-as (ax1) voor de prijs + fig, ax1 = plt.subplots(figsize=(15, 8)) + + # Plot de Voorspelde Prijs op de linker-as (ax1) + ax1.plot( + resultaat_df.index, + resultaat_df['Voorspelde_Prijs'], + color='blue', + marker='.', + linestyle='-', + linewidth=2, + label='Voorspelde Prijs' + ) + + # --- NIEUW: Plot werkelijke prijzen als die beschikbaar zijn --- + if werkelijke_prijzen_df is not None and not werkelijke_prijzen_df.empty: + ax1.plot( + werkelijke_prijzen_df.index, + werkelijke_prijzen_df['price'], + color='orange', + marker='o', + linestyle='--', + linewidth=2, + label=f'Werkelijke Prijs ({LEVERANCIER})' + ) + print(f"āœ… Werkelijke prijzen toegevoegd aan grafiek ({len(werkelijke_prijzen_df)} punten).") + + ax1.set_ylabel('Prijs (€)', color='blue', fontsize=12) + ax1.tick_params(axis='y', labelcolor='blue') + ax1.set_xlabel('Datum en Tijd', fontsize=12) + ax1.grid(True, which='major', axis='x', alpha=0.3) + + # Maak de TWEEDE Y-as (ax2) die de X-as deelt + ax2 = ax1.twinx() + + weer_toekomst = werk_df.loc[te_voorspellen_tijden] + + # Plot Temperatuur op de rechter-as (ax2) + ax2.plot( + weer_toekomst.index, + weer_toekomst['temperatuur'], + color='red', + marker='.', + linestyle='--', + alpha=0.6, + label='Temperatuur (°C)' + ) + # Plot Windsnelheid op de rechter-as (ax2) + ax2.plot( + weer_toekomst.index, + weer_toekomst['wind_snelheid'], + color='green', + marker='x', + linestyle=':', + alpha=0.6, + label='Windsnelheid (km/u)' + ) + ax2.set_ylabel('Temperatuur / Windsnelheid', color='black', fontsize=12) + ax2.tick_params(axis='y', labelcolor='black') + + # Titel en gecombineerde legenda + plt.title(f'Energieprijs Voorspelling vs Werkelijk ({len(resultaat_df)} uur)', fontsize=16, fontweight='bold') + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax2.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=10) + + # X-as opmaken (Nederlands formaat) + date_format = mdates.DateFormatter('%d-%m %H:%M') + ax1.xaxis.set_major_formatter(date_format) + plt.setp(ax1.get_xticklabels(), rotation=30, ha='right') + + fig.tight_layout() + + # Sla de grafiek op + plt.savefig(GRAFIEK_BESTAND, dpi=150) + plt.close(fig) + print(f"āœ… Grafiek opgeslagen als: {GRAFIEK_BESTAND}") + + # Stuur de e-mail + send_email_with_graph(GRAFIEK_BESTAND, resultaat_df) + + except Exception as e: + print(f"āŒ Fout bij genereren van grafiek: {e}") + import traceback + traceback.print_exc() + +except Error as e: + print(f"āŒ Fout met MySQL verbinding: {e}") +except Exception as e: + print(f"āŒ Een onverwachte fout is opgetreden: {e}") + import traceback + traceback.print_exc() +finally: + if 'conn' in locals() and conn.is_connected(): + conn.close() + print("\nVerbinding met MySQL gesloten.") \ No newline at end of file diff --git a/prijs_voorspelling.png b/prijs_voorspelling.png new file mode 100644 index 0000000..40da85b Binary files /dev/null and b/prijs_voorspelling.png differ