From 442329bb34072ffcf9b721462f3a6a5c46d89a71 Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sun, 22 Feb 2026 04:12:16 +0530 Subject: [PATCH 1/2] Fix delta_kt_prime calculation for series with internal NaN values The previous implementation of _delta_kt_prime_dirint only handled NaN at the very first and last positions of the series. For multi-day data with nighttime NaN gaps, the edge positions adjacent to internal NaN boundaries had their delta values incorrectly halved (the 0.5 factor was applied even when only one neighbor was valid). Replace the manual NaN-filling approach with pd.DataFrame.mean(axis=1), which naturally skips NaN values. This correctly implements both Perez eqn 2 (average of both deltas when two neighbors exist) and eqn 3 (single delta when only one neighbor exists). Fixes #1847 --- docs/sphinx/source/whatsnew/v0.15.1.rst | 6 +++++ pvlib/irradiance.py | 15 ++++++------ tests/test_irradiance.py | 32 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.1.rst b/docs/sphinx/source/whatsnew/v0.15.1.rst index 09bb0d01dd..9345400fa5 100644 --- a/docs/sphinx/source/whatsnew/v0.15.1.rst +++ b/docs/sphinx/source/whatsnew/v0.15.1.rst @@ -14,6 +14,12 @@ Deprecations Bug fixes ~~~~~~~~~ +* Fix incorrect ``delta_kt_prime`` calculation in + :py:func:`pvlib.irradiance.dirint` for series containing internal NaN + values (e.g. multi-day data with nighttime gaps). Edge positions with + only one valid neighbor now correctly use that single delta value + (Perez eqn 3) instead of halving it. + (:issue:`1847`, :pull:`xxxx`) * Fix a division-by-zero condition in :py:func:`pvlib.transformer.simple_efficiency` when ``load_loss = 0``. (:issue:`2645`, :pull:`2646`) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index c67c1ca251..8399f53dbb 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -2026,16 +2026,15 @@ def _delta_kt_prime_dirint(kt_prime, use_delta_kt_prime, times): for use with :py:func:`_dirint_bins`. """ if use_delta_kt_prime: - # Perez eqn 2 + # Perez eqn 2 (both neighbors) and eqn 3 (one neighbor). + # mean(axis=1) skips NaN so that edge positions with only one + # valid neighbor return that single delta instead of halving it. kt_next = kt_prime.shift(-1) kt_previous = kt_prime.shift(1) - # replace nan with values that implement Perez Eq 3 for first and last - # positions. Use kt_previous and kt_next to handle series of length 1 - kt_next.iloc[-1] = kt_previous.iloc[-1] - kt_previous.iloc[0] = kt_next.iloc[0] - delta_kt_prime = 0.5 * ((kt_prime - kt_next).abs().add( - (kt_prime - kt_previous).abs(), - fill_value=0)) + delta_kt_prime = pd.DataFrame({ + 'next': (kt_prime - kt_next).abs(), + 'prev': (kt_prime - kt_previous).abs(), + }).mean(axis=1) else: # do not change unless also modifying _dirint_bins delta_kt_prime = pd.Series(-1, index=times) diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index a416636ae9..9b02594256 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -743,6 +743,38 @@ def test_dirint_no_delta_kt(): np.array([861.9, 670.4]), 1) +def test_delta_kt_prime_dirint_multiday(): + # GH 1847: _delta_kt_prime_dirint mishandled NaN boundaries between + # days, halving the delta at edge positions instead of using the + # single available neighbor value (Perez eqn 3). + times = pd.date_range('2014-01-01T05', periods=15, freq='1h', + tz='Etc/GMT+0') + kt_prime = pd.Series( + [np.nan, np.nan, np.nan, + 0.29458475, 0.21863506, 0.37650014, + 0.41238529, 0.23375275, 0.23363453, + 0.26348652, 0.25412631, 0.43794681, + np.nan, np.nan, np.nan], + index=times) + result = irradiance._delta_kt_prime_dirint(kt_prime, True, times) + # first valid value (index 3): only forward neighbor exists, + # delta_kt_prime == |kt[3] - kt[4]| (Perez eqn 3) + expected_first = abs(kt_prime.iloc[3] - kt_prime.iloc[4]) + assert_almost_equal(result.iloc[3], expected_first) + # last valid value (index 11): only backward neighbor exists, + # delta_kt_prime == |kt[11] - kt[10]| (Perez eqn 3) + expected_last = abs(kt_prime.iloc[11] - kt_prime.iloc[10]) + assert_almost_equal(result.iloc[11], expected_last) + # interior valid value (index 6): both neighbors exist, + # delta_kt_prime == mean of both deltas (Perez eqn 2) + expected_mid = 0.5 * (abs(kt_prime.iloc[6] - kt_prime.iloc[7]) + + abs(kt_prime.iloc[6] - kt_prime.iloc[5])) + assert_almost_equal(result.iloc[6], expected_mid) + # NaN positions should remain NaN + assert np.isnan(result.iloc[0]) + assert np.isnan(result.iloc[-1]) + + def test_dirint_coeffs(): coeffs = irradiance._get_dirint_coeffs() assert coeffs[0, 0, 0, 0] == 0.385230 From 46baf52da552b2f80d59bf05167a4f58a84c005a Mon Sep 17 00:00:00 2001 From: ishaan-arora-1 Date: Sun, 22 Feb 2026 04:13:12 +0530 Subject: [PATCH 2/2] Update whatsnew with PR number and contributor name --- docs/sphinx/source/whatsnew/v0.15.1.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.1.rst b/docs/sphinx/source/whatsnew/v0.15.1.rst index 9345400fa5..fe2578d25a 100644 --- a/docs/sphinx/source/whatsnew/v0.15.1.rst +++ b/docs/sphinx/source/whatsnew/v0.15.1.rst @@ -19,7 +19,7 @@ Bug fixes values (e.g. multi-day data with nighttime gaps). Edge positions with only one valid neighbor now correctly use that single delta value (Perez eqn 3) instead of halving it. - (:issue:`1847`, :pull:`xxxx`) + (:issue:`1847`, :pull:`2698`) * Fix a division-by-zero condition in :py:func:`pvlib.transformer.simple_efficiency` when ``load_loss = 0``. (:issue:`2645`, :pull:`2646`) @@ -63,6 +63,7 @@ Maintenance Contributors ~~~~~~~~~~~~ +* Ishaan Arora (:ghuser:`ishaan-arora-1`) * Aman Srivastava (:ghuser:`aman-coder03`) * Rajiv Daxini (:ghuser:`RDaxini`) * Echedey Luis (:ghuser:`echedey-ls`)