From 72cd6445282190bef0efcf447599204a8404d64d Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 3 Jan 2026 20:42:22 +0100 Subject: [PATCH] Fix: Interpolate missing ENTSOE price data points Handle missing hourly price data from ENTSOE API by interpolating values from adjacent time slots instead of returning nil. --- .gitignore | 1 + app/models/entsoe.rb | 50 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index efe1657..12e19dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .config .project *.pid +data diff --git a/app/models/entsoe.rb b/app/models/entsoe.rb index 4090a44..51edfca 100644 --- a/app/models/entsoe.rb +++ b/app/models/entsoe.rb @@ -103,15 +103,61 @@ class Entsoe formatted_date = date.strftime("%F") doc = Nokogiri::XML(URI.open(url)) + + #p "Entsoe prices: %s" % doc prices = doc.xpath('.//xmlns:Point').map{|p| parse_point(p)} begin # get start_time (in UTC) from XML docment start_time = DateTime.parse(doc.xpath('.//xmlns:period.timeInterval//xmlns:start').text) + # Get resolution to determine expected number of positions + resolution = doc.xpath('.//xmlns:resolution').text + interval_minutes = resolution == 'PT15M' ? 15 : 60 + expected_positions = (24 * 60) / interval_minutes + + # Create hash from available prices + price_hash = prices.to_h + + # Fill in missing positions by interpolating from adjacent values + complete_prices = {} + (1..expected_positions).each do |position| + if price_hash.key?(position) + complete_prices[position] = price_hash[position] + else + # Find previous and next available prices for interpolation + prev_price = (position-1).downto(1).find { |p| price_hash.key?(p) } + next_price = (position+1).upto(expected_positions).find { |p| price_hash.key?(p) } + + if prev_price && next_price + # Interpolate between previous and next + complete_prices[position] = ((price_hash[prev_price] + price_hash[next_price]) / 2.0).round(5) + elsif prev_price + # Use previous price as fallback + complete_prices[position] = price_hash[prev_price] + elsif next_price + # Use next price as fallback + complete_prices[position] = price_hash[next_price] + end + + # Calculate the formatted hour for the warning message + hour_offset = interval_minutes == 15 ? (position - 1) / 4 : (position - 1) + missing_hour = start_time.advance(hours: hour_offset).in_time_zone(zone).strftime("%F %H") + p "Warning: Missing Entsoe data for #{missing_hour}, interpolated value: #{complete_prices[position]}" + end + end + #returns a hash with keys formatted "yyyy-mm-dd hr" and values price (per kwh) - # tag runs from 1-24. We need hours from 00-23, therefore substracting 1 - prices.map{|p| [start_time.advance(hours: (p[0]-1)), p[1]]}.to_h + # tag runs from 1-96 for 15min intervals. Convert to hourly by taking first interval of each hour + if interval_minutes == 15 + # For 15-minute intervals, use the first interval of each hour (positions 1, 5, 9, 13, ...) + complete_prices.select { |pos, _| (pos - 1) % 4 == 0 } + .map { |pos, price| [start_time.advance(hours: ((pos - 1) / 4)), price] } + .to_h + else + # For hourly data (position runs from 1-24, we need hours from 00-23) + complete_prices.map { |pos, price| [start_time.advance(hours: (pos - 1)), price] }.to_h + end rescue Date::Error => e p e.message {}