From 630e584481541e90152d8c865e07acf19fdc08bb Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:43:20 +0200 Subject: [PATCH 01/19] Fix deprecated inplace operations --- pvlib/iotools/pvgis.py | 2 +- pvlib/iotools/solargis.py | 5 ++--- pvlib/iotools/solcast.py | 3 +-- pvlib/iotools/surfrad.py | 2 +- pvlib/ivtools/utils.py | 2 +- pvlib/snow.py | 2 +- tests/spectrum/test_response.py | 3 +-- tests/test_modelchain.py | 2 +- tests/test_shading.py | 5 ++--- 9 files changed, 11 insertions(+), 15 deletions(-) diff --git a/pvlib/iotools/pvgis.py b/pvlib/iotools/pvgis.py index 61f3eff695..74b044abad 100644 --- a/pvlib/iotools/pvgis.py +++ b/pvlib/iotools/pvgis.py @@ -766,7 +766,7 @@ def get_pvgis_horizon(latitude, longitude, url=URL, **kwargs): data.columns = ['horizon_azimuth', 'horizon_elevation'] # Convert azimuth to pvlib convention (north=0, south=180) data['horizon_azimuth'] += 180 - data.set_index('horizon_azimuth', inplace=True) + data = data.set_index('horizon_azimuth') data = data['horizon_elevation'] # convert to pd.Series data = data[data.index < 360] # remove duplicate north point (0 and 360) return data, metadata diff --git a/pvlib/iotools/solargis.py b/pvlib/iotools/solargis.py index 375c7ed3e8..2792011c2e 100644 --- a/pvlib/iotools/solargis.py +++ b/pvlib/iotools/solargis.py @@ -204,9 +204,8 @@ def get_solargis(latitude, longitude, start, end, variables, api_key, if map_variables: for variable in VARIABLE_MAP: if variable.solargis_name in data.columns: - data.rename( - columns={variable.solargis_name: variable.pvlib_name}, - inplace=True + data = data.rename( + columns={variable.solargis_name: variable.pvlib_name} ) data[variable.pvlib_name] = data[ variable.pvlib_name].apply(variable.conversion) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 451ef58561..01a4081fef 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -421,9 +421,8 @@ def _solcast2pvlib(data): # rename and convert variables for variable in VARIABLE_MAP: if variable.solcast_name in data.columns: - data.rename( + data = data.rename( columns={variable.solcast_name: variable.pvlib_name}, - inplace=True ) data[variable.pvlib_name] = data[ variable.pvlib_name].apply(variable.conversion) diff --git a/pvlib/iotools/surfrad.py b/pvlib/iotools/surfrad.py index 948ec5e09e..9c33c07567 100644 --- a/pvlib/iotools/surfrad.py +++ b/pvlib/iotools/surfrad.py @@ -158,7 +158,7 @@ def read_surfrad(filename, map_variables=True): data = data.where(~missing, np.nan) if map_variables: - data.rename(columns=VARIABLE_MAP, inplace=True) + data = data.rename(columns=VARIABLE_MAP) return data, metadata diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py index 395678d49c..0d0ef1382a 100644 --- a/pvlib/ivtools/utils.py +++ b/pvlib/ivtools/utils.py @@ -160,7 +160,7 @@ def rectify_iv_curve(voltage, current, decimals=None): df = pd.DataFrame(data=np.vstack((voltage, current)).T, columns=['v', 'i']) # restrict to first quadrant - df.dropna(inplace=True) + df = df.dropna() df = df[(df['v'] >= 0) & (df['i'] >= 0)] # sort pairs on voltage, then current df = df.sort_values(by=['v', 'i'], ascending=[True, False]) diff --git a/pvlib/snow.py b/pvlib/snow.py index b78a93a2ec..4492abc554 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -170,7 +170,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, snow_coverage[new_snowfall] = 1.0 if np.isnan(snow_coverage.iloc[0]): snow_coverage.iloc[0] = initial_coverage - snow_coverage.ffill(inplace=True) + snow_coverage = snow_coverage.ffill() snow_coverage -= cumulative_sliding return snow_coverage.clip(lower=0) diff --git a/tests/spectrum/test_response.py b/tests/spectrum/test_response.py index 2ffe572dce..113f09cc0c 100644 --- a/tests/spectrum/test_response.py +++ b/tests/spectrum/test_response.py @@ -45,8 +45,7 @@ def sr_and_eqe_fixture(): [1050, 0.4, 0.338752845368667], ], ) - df.set_index("wavelength", inplace=True) - return df + return df.set_index("wavelength") def test_sr_to_qe(sr_and_eqe_fixture): diff --git a/tests/test_modelchain.py b/tests/test_modelchain.py index 15d580aa6c..6b443ac0fc 100644 --- a/tests/test_modelchain.py +++ b/tests/test_modelchain.py @@ -602,7 +602,7 @@ def test_prepare_inputs_missing_irrad_component( sapm_dc_snl_ac_system, location, missing): mc = ModelChain(sapm_dc_snl_ac_system, location) weather = pd.DataFrame({'dhi': [1, 2], 'dni': [1, 2], 'ghi': [1, 2]}) - weather.drop(columns=missing, inplace=True) + weather = weather.drop(columns=missing) with pytest.raises(ValueError): mc.prepare_inputs(weather) diff --git a/tests/test_shading.py b/tests/test_shading.py index f83f2db47b..82afcb88eb 100644 --- a/tests/test_shading.py +++ b/tests/test_shading.py @@ -270,7 +270,7 @@ def sf1d_premises_and_expected(): (test_data["theta_L"], test_data["theta_R"]), (test_data["theta_R"], test_data["theta_L"]), ) - test_data.rename( + test_data = test_data.rename( columns={ "theta_L": "shading_row_rotation", "theta_R": "shaded_row_rotation", @@ -279,9 +279,8 @@ def sf1d_premises_and_expected(): "theta_s": "solar_zenith", # for the projected solar zenith angle "f_s": "shaded_fraction", }, - inplace=True, ) - test_data.drop(columns=["x_L", "z_L", "x_R", "z_R"], inplace=True) + test_data = test_data.drop(columns=["x_L", "z_L", "x_R", "z_R"]) # for the projected solar zenith angle # this returns the same psz angle as test_data["solar_zenith"] test_data["solar_azimuth"], test_data["axis_azimuth"] = 180, 90 From 889bcb14fb39ca65beb484ec30160638b127e832 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:53:38 +0200 Subject: [PATCH 02/19] Fix Pandas4Warning: .dayofyear -> .day_of_year Pandas4Warning: DatetimeIndex.dayofyear is deprecated and will be removed in a future version. Use DatetimeIndex.day_of_year instead. --- benchmarks/benchmarks/solarposition.py | 2 +- .../solar-position/plot_sunpath_diagrams.py | 12 ++++---- pvlib/clearsky.py | 2 +- pvlib/iotools/solrad.py | 2 +- pvlib/iotools/surfrad.py | 2 +- pvlib/irradiance.py | 12 ++++---- pvlib/solarposition.py | 2 +- pvlib/tools.py | 2 +- tests/test_clearsky.py | 4 +-- tests/test_irradiance.py | 2 +- tests/test_solarposition.py | 28 +++++++++---------- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/benchmarks/benchmarks/solarposition.py b/benchmarks/benchmarks/solarposition.py index 0ccb379d8c..c3e101955a 100644 --- a/benchmarks/benchmarks/solarposition.py +++ b/benchmarks/benchmarks/solarposition.py @@ -51,7 +51,7 @@ def time_sun_rise_set_transit_ephem(self, ndays): self.times_daily, self.lat, self.lon) def time_sun_rise_set_transit_geometric_full_comparison(self, ndays): - dayofyear = self.times_daily.dayofyear + dayofyear = self.times_daily.day_of_year declination = solarposition.declination_spencer71(dayofyear) equation_of_time = solarposition.equation_of_time_spencer71(dayofyear) solarposition.sun_rise_set_transit_geometric( diff --git a/docs/examples/solar-position/plot_sunpath_diagrams.py b/docs/examples/solar-position/plot_sunpath_diagrams.py index 282a8dee43..20de978423 100644 --- a/docs/examples/solar-position/plot_sunpath_diagrams.py +++ b/docs/examples/solar-position/plot_sunpath_diagrams.py @@ -32,13 +32,13 @@ ax = plt.subplot(1, 1, 1, projection='polar') # draw the analemma loops points = ax.scatter(np.radians(solpos.azimuth), solpos.apparent_zenith, - s=2, label=None, c=solpos.index.dayofyear, + s=2, label=None, c=solpos.index.day_of_year, cmap='twilight_shifted_r') # add and format colorbar cbar = ax.figure.colorbar(points) times_ticks = pd.date_range('2019-01-01', '2020-01-01', freq='MS', tz=tz) -cbar.set_ticks(ticks=times_ticks.dayofyear, labels=[], minor=False) -cbar.set_ticks(ticks=times_ticks.dayofyear+15, +cbar.set_ticks(ticks=times_ticks.day_of_year, labels=[], minor=False) +cbar.set_ticks(ticks=times_ticks.day_of_year+15, labels=times_ticks.strftime('%b'), minor=True) cbar.ax.tick_params(which='minor', width=0) @@ -120,13 +120,13 @@ fig, ax = plt.subplots() points = ax.scatter(solpos.azimuth, solpos.apparent_elevation, s=2, - c=solpos.index.dayofyear, label=None, + c=solpos.index.day_of_year, label=None, cmap='twilight_shifted_r') # add and format colorbar cbar = fig.colorbar(points) times_ticks = pd.date_range('2019-01-01', '2020-01-01', freq='MS', tz=tz) -cbar.set_ticks(ticks=times_ticks.dayofyear, labels=[], minor=False) -cbar.set_ticks(ticks=times_ticks.dayofyear+15, +cbar.set_ticks(ticks=times_ticks.day_of_year, labels=[], minor=False) +cbar.set_ticks(ticks=times_ticks.day_of_year+15, labels=times_ticks.strftime('%b'), minor=True) cbar.ax.tick_params(which='minor', width=0) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 87c8c78f86..3af86fa771 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -245,7 +245,7 @@ def _interpolate_turbidity(lts, time): isleap = time_utc.is_leap_year - dayofyear = time_utc.dayofyear + dayofyear = time_utc.day_of_year days_leap = _calendar_month_middles(2016) days_no_leap = _calendar_month_middles(2015) diff --git a/pvlib/iotools/solrad.py b/pvlib/iotools/solrad.py index f4d446fc61..8e661b74ad 100644 --- a/pvlib/iotools/solrad.py +++ b/pvlib/iotools/solrad.py @@ -198,7 +198,7 @@ def get_solrad(station, start, end, dates = pd.date_range(start.floor('D'), end, freq='D') station = station.lower() filenames = [ - f"{station}/{d.year}/{station}{d.strftime('%y')}{d.dayofyear:03}.dat" + f"{station}/{d.year}/{station}{d.strftime('%y')}{d.day_of_year:03}.dat" for d in dates ] diff --git a/pvlib/iotools/surfrad.py b/pvlib/iotools/surfrad.py index 9c33c07567..d25651e19a 100644 --- a/pvlib/iotools/surfrad.py +++ b/pvlib/iotools/surfrad.py @@ -250,7 +250,7 @@ def get_surfrad(station, start, end, map_variables=True, station = station.lower() filenames = [ - f"{station}/{d.year}/{station}{d.strftime('%y')}{d.dayofyear:03}.dat" # noqa: E231,E501 + f"{station}/{d.year}/{station}{d.strftime('%y')}{d.day_of_year:03}.dat" # noqa: E231,E501 for d in dates ] diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 38bf122b91..475dc9bed7 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -1937,7 +1937,7 @@ def disc(ghi, solar_zenith, datetime_or_doy, pressure=101325, datetime_or_doy : int, float, array, pd.DatetimeIndex Day of year or array of days of year e.g. - pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex. + pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. pressure : numeric or None, default 101325 Site pressure. See :term:`pressure`. [Pa] @@ -2765,7 +2765,7 @@ def erbs(ghi, zenith, datetime_or_doy, min_cos_zenith=0.065, max_zenith=87): :term:`solar_zenith`. [°] datetime_or_doy : int, float, array, pd.DatetimeIndex Day of year or array of days of year e.g. - pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex. + pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. min_cos_zenith : numeric, default 0.065 Minimum value of cos(zenith) to allow when calculating global clearness index `kt`. Equivalent to zenith = 86.273°. @@ -2865,7 +2865,7 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, datetime_or_doy : int, float, array or pd.DatetimeIndex, optional Day of year or array of days of year e.g. - pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex. + pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. Either ``datetime_or_doy`` or ``dni_extra`` must be provided. dni_extra : numeric, optional @@ -2992,7 +2992,7 @@ def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None, datetime_or_doy : int, float, array or pd.DatetimeIndex, optional Day of year or array of days of year e.g. - pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex. + pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. Either ``datetime_or_doy`` or ``dni_extra`` must be provided. dni_extra : numeric, optional @@ -3093,7 +3093,7 @@ def boland(ghi, solar_zenith, datetime_or_doy, a_coeff=8.645, b_coeff=0.613, :term:`solar_zenith`. [°] datetime_or_doy : numeric or pd.DatetimeIndex. - Day of year or array of days of year e.g. pd.DatetimeIndex.dayofyear, + Day of year or array of days of year e.g. pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. a_coeff : float, default 8.645 @@ -3897,7 +3897,7 @@ def louche(ghi, solar_zenith, datetime_or_doy, max_zenith=90): datetime_or_doy : numeric, pandas.DatetimeIndex Day of year or array of days of year e.g. - pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex. + pd.DatetimeIndex.day_of_year, or pd.DatetimeIndex. Returns ------- diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 4441ed01f1..eb3bcdc81d 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -772,7 +772,7 @@ def ephemeris(time, latitude, longitude, pressure=101325.0, temperature=12.0): time_utc = tools._pandas_to_utc(time) # strip out the day of the year and calculate the decimal hour - DayOfYear = time_utc.dayofyear + DayOfYear = time_utc.day_of_year DecHours = (time_utc.hour + time_utc.minute/60. + time_utc.second/3600. + time_utc.microsecond/3600.e6) diff --git a/pvlib/tools.py b/pvlib/tools.py index 1d9db70369..b519715cb3 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -214,7 +214,7 @@ def _pandas_to_doy(pd_object): ----- Day of year is determined using UTC, since pandas uses local hour """ - return _pandas_to_utc(pd_object).dayofyear + return _pandas_to_utc(pd_object).day_of_year def _pandas_to_utc(pd_object): diff --git a/tests/test_clearsky.py b/tests/test_clearsky.py index 0e49b32911..138cc43f26 100644 --- a/tests/test_clearsky.py +++ b/tests/test_clearsky.py @@ -782,9 +782,9 @@ def test_bird(): aod_380nm = 0.15 b_a = 0.85 alb = 0.2 - eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) + eot = solarposition.equation_of_time_spencer71(times_utc.day_of_year) hour_angle = solarposition.hour_angle(times, longitude, eot) - 0.5 * 15. - declination = solarposition.declination_spencer71(times_utc.dayofyear) + declination = solarposition.declination_spencer71(times_utc.day_of_year) zenith = solarposition.solar_zenith_analytical( np.deg2rad(latitude), np.deg2rad(hour_angle), declination ) diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index b0d9863ad5..1ee62db6f8 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -72,7 +72,7 @@ def relative_airmass(times): # setup for et rad test. put it here for readability timestamp = pd.Timestamp('20161026') dt_index = pd.DatetimeIndex([timestamp]) -doy = timestamp.dayofyear +doy = timestamp.day_of_year dt_date = timestamp.date() dt_datetime = datetime.datetime.combine(dt_date, datetime.time(0)) dt_np64 = np.datetime64(dt_datetime) diff --git a/tests/test_solarposition.py b/tests/test_solarposition.py index 9dc0488fe5..ad483eef67 100644 --- a/tests/test_solarposition.py +++ b/tests/test_solarposition.py @@ -618,8 +618,8 @@ def test_equation_of_time(): output = solarposition.spa_python(times, 37.8, -122.25, 100) eot = output['equation_of_time'] eot_rng = eot.max() - eot.min() # range of values, around 30 minutes - eot_1 = solarposition.equation_of_time_spencer71(times.dayofyear) - eot_2 = solarposition.equation_of_time_pvcdrom(times.dayofyear) + eot_1 = solarposition.equation_of_time_spencer71(times.day_of_year) + eot_2 = solarposition.equation_of_time_pvcdrom(times.day_of_year) assert np.allclose(eot_1 / eot_rng, eot / eot_rng, atol=0.3) # spencer assert np.allclose(eot_2 / eot_rng, eot / eot_rng, atol=0.4) # pvcdrom @@ -635,8 +635,8 @@ def test_declination(): sst=True) declination = np.deg2rad(declination) declination_rng = declination.max() - declination.min() - declination_1 = solarposition.declination_cooper69(times.dayofyear) - declination_2 = solarposition.declination_spencer71(times.dayofyear) + declination_1 = solarposition.declination_cooper69(times.day_of_year) + declination_2 = solarposition.declination_spencer71(times.day_of_year) a, b = declination_1 / declination_rng, declination / declination_rng assert np.allclose(a, b, atol=0.03) # cooper a, b = declination_2 / declination_rng, declination / declination_rng @@ -652,14 +652,14 @@ def test_analytical_zenith(): output = solarposition.spa_python(times, lat, lon, 100) solar_zenith = np.deg2rad(output['zenith']) # spa # spencer - eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) + eot = solarposition.equation_of_time_spencer71(times_utc.day_of_year) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) - decl = solarposition.declination_spencer71(times_utc.dayofyear) + decl = solarposition.declination_spencer71(times_utc.day_of_year) zenith_1 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) # pvcdrom and cooper - eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear) + eot = solarposition.equation_of_time_pvcdrom(times_utc.day_of_year) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) - decl = solarposition.declination_cooper69(times_utc.dayofyear) + decl = solarposition.declination_cooper69(times_utc.day_of_year) zenith_2 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) assert np.allclose(zenith_1, solar_zenith, atol=0.015) assert np.allclose(zenith_2, solar_zenith, atol=0.025) @@ -675,16 +675,16 @@ def test_analytical_azimuth(): solar_azimuth = np.deg2rad(output['azimuth']) # spa solar_zenith = np.deg2rad(output['zenith']) # spencer - eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) + eot = solarposition.equation_of_time_spencer71(times_utc.day_of_year) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) - decl = solarposition.declination_spencer71(times_utc.dayofyear) + decl = solarposition.declination_spencer71(times_utc.day_of_year) zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, decl, zenith) # pvcdrom and cooper - eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear) + eot = solarposition.equation_of_time_pvcdrom(times_utc.day_of_year) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) - decl = solarposition.declination_cooper69(times_utc.dayofyear) + decl = solarposition.declination_cooper69(times_utc.day_of_year) zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, decl, zenith) @@ -814,8 +814,8 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): latitude = golden_mst.latitude longitude = golden_mst.longitude eot = solarposition.equation_of_time_spencer71( - times_utc.dayofyear) # minutes - decl = solarposition.declination_spencer71(times_utc.dayofyear) # radians + times_utc.day_of_year) # minutes + decl = solarposition.declination_spencer71(times_utc.day_of_year) # radians with pytest.raises(ValueError): solarposition.sun_rise_set_transit_geometric( times.tz_convert(None), latitude=latitude, longitude=longitude, From 9f3ae9b87efde78baa9ecf57f43e95b5679e3fcd Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:59:28 +0200 Subject: [PATCH 03/19] Fix Pandas4Warning: deprecated passing a list to DataFrame.from_dict -> DataFrame constructor instead Pandas4Warning: Passing a list to DataFrame.from_dict is deprecated. Use the DataFrame constructor instead. --- pvlib/iotools/solcast.py | 2 +- tests/iotools/test_solcast.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 01a4081fef..96ed677097 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -473,7 +473,7 @@ def _get_solcast( if response.status_code == 200: j = response.json() - df = pd.DataFrame.from_dict(j[list(j.keys())[0]]) + df = pd.DataFrame(j[list(j.keys())[0]]) if map_variables: return _solcast2pvlib(df) else: diff --git a/tests/iotools/test_solcast.py b/tests/iotools/test_solcast.py index 23989ecb2e..d6df24c222 100644 --- a/tests/iotools/test_solcast.py +++ b/tests/iotools/test_solcast.py @@ -39,7 +39,7 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): endpoint, params, api_key, True ), pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]]) ) ) @@ -49,7 +49,7 @@ def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): pvlib.iotools.solcast._get_solcast( endpoint, params, api_key, False ), - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]]) ) @@ -93,7 +93,7 @@ def test_get_solcast_live( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ) ), @@ -101,7 +101,7 @@ def test_get_solcast_live( else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ), ) @@ -145,7 +145,7 @@ def test_get_solcast_tmy( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ) ), @@ -153,7 +153,7 @@ def test_get_solcast_tmy( else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ), ) @@ -232,7 +232,7 @@ def test_get_solcast_historic( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ) ), @@ -240,7 +240,7 @@ def test_get_solcast_historic( else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ), ) @@ -288,7 +288,7 @@ def test_get_solcast_forecast( pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], pvlib.iotools.solcast._solcast2pvlib( - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ) ), @@ -296,7 +296,7 @@ def test_get_solcast_forecast( else: pd.testing.assert_frame_equal( function(**params, map_variables=map_variables)[0], - pd.DataFrame.from_dict( + pd.DataFrame( json_response[list(json_response.keys())[0]] ), ) From 279dec735b0faacdee73e1828e6ad1703b72a6c0 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:05:41 +0200 Subject: [PATCH 04/19] Fix Pandas4Warning: assert_index_equal will check the 'freq' attribute Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs and None do not match. Pass check_freq=True or check_freq=False to silence this warning pd.testing.assert_index_equal(left, right, **kwargs) --- tests/conftest.py | 2 +- tests/iotools/test_solaranywhere.py | 6 +++++- tests/iotools/test_tmy.py | 6 +++++- tests/test_conftest.py | 5 +++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8a1c1180d8..bda29842ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ def _check_pandas_assert_kwargs(kwargs): def assert_index_equal(left, right, **kwargs): kwargs = _check_pandas_assert_kwargs(kwargs) - pd.testing.assert_index_equal(left, right, **kwargs) + pd.testing.assert_index_equal(left, right, **kwargs, check_freq=False) def assert_series_equal(left, right, **kwargs): diff --git a/tests/iotools/test_solaranywhere.py b/tests/iotools/test_solaranywhere.py index 54216a7ab0..c4ff5858c3 100644 --- a/tests/iotools/test_solaranywhere.py +++ b/tests/iotools/test_solaranywhere.py @@ -74,7 +74,11 @@ def test_read_solaranywhere_high_resolution(high_resolution_index): assert data.loc['2021-01-01 07:00:00-05:00', 'Albedo'] == 0.6 assert data.loc['2021-01-01 07:00:00-05:00', 'WindSpeed (m/s)'] == 0 # Assert that the index is parsed correctly - pd.testing.assert_index_equal(data.index, high_resolution_index) + pd.testing.assert_index_equal( + data.index, + high_resolution_index, + check_freq=False, + ) def test_read_solaranywhere_map_variables(): diff --git a/tests/iotools/test_tmy.py b/tests/iotools/test_tmy.py index 02ffe7042f..0659c416f6 100644 --- a/tests/iotools/test_tmy.py +++ b/tests/iotools/test_tmy.py @@ -111,7 +111,11 @@ def test_solaranywhere_tmy3(solaranywhere_index): # included to test files with 00:00 timestamps are parsed correctly data, meta = tmy.read_tmy3(TMY3_SOLARANYWHERE, encoding='iso-8859-1', map_variables=False) - pd.testing.assert_index_equal(data.index, solaranywhere_index) + pd.testing.assert_index_equal( + data.index, + solaranywhere_index, + check_freq=False, + ) assert meta['USAF'] == 0 assert meta['Name'] == 'Burlington United States' assert meta['State'] == 'NA' diff --git a/tests/test_conftest.py b/tests/test_conftest.py index fc8b14c31c..dbc9cedaaf 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -33,5 +33,10 @@ def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, expected_kwargs = {'atol': tol, 'rtol': tol} else: expected_kwargs = {'check_less_precise': check_less_precise} + if function_name == "assert_index_equal": + # assert_index_equal in Pandas 3.1.0 warns: + # Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs and None do not match. Pass check_freq=True or check_freq=False to silence this warning + # pd.testing.assert_index_equal(left, right, **kwargs) + expected_kwargs['check_freq'] = False spy.assert_called_once_with(*args, **expected_kwargs) From b2aba024b6d6b52cbdcc011b42d7e333df36a683 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:52:59 +0100 Subject: [PATCH 05/19] Fix pandas 3.1.0 unexpected behavior on .map (may defer until pandas GH#71567 is resolved) pvlib/iotools/tmy.py:226: in read_tmy3 data_ymd.iloc[-1] = data_ymd.iloc[-1].replace(year=coerce_year+1) ^^^^^^^^^^^^^^^^^ venv/lib/python3.14/site-packages/pandas/core/indexing.py:1010: in __setitem__ iloc._setitem_with_indexer(indexer, value, self.name) venv/lib/python3.14/site-packages/pandas/core/indexing.py:2653: in _setitem_with_indexer self._setitem_single_block(indexer, value, name) venv/lib/python3.14/site-packages/pandas/core/indexing.py:2985: in _setitem_single_block self.obj._mgr = self.obj._mgr.setitem(indexer=indexer, value=value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ venv/lib/python3.14/site-packages/pandas/core/internals/managers.py:625: in setitem return self.apply("setitem", indexer=indexer, value=value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ venv/lib/python3.14/site-packages/pandas/core/internals/managers.py:456: in apply applied = getattr(b, f)(**kwargs) ^^^^^^^^^^^^^^^^^^^^^^^ --- pvlib/iotools/tmy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/tmy.py b/pvlib/iotools/tmy.py index 28d20ffc2d..2dcd2b529f 100644 --- a/pvlib/iotools/tmy.py +++ b/pvlib/iotools/tmy.py @@ -222,7 +222,7 @@ def read_tmy3(filename, coerce_year=None, map_variables=True, encoding=None): # shifted_hour is a pd.Series, so use pd.to_timedelta to get a pd.Series of # timedeltas if coerce_year is not None: - data_ymd = data_ymd.map(lambda dt: dt.replace(year=coerce_year)) + data_ymd = data_ymd.transform(lambda dt: dt.replace(year=coerce_year)) data_ymd.iloc[-1] = data_ymd.iloc[-1].replace(year=coerce_year+1) # NOTE: as of pvlib-0.6.3, min req is pandas-0.18.1, so pd.to_timedelta # unit must be in (D,h,m,s,ms,us,ns), but pandas>=0.24 allows unit='hour' From 65371c88827eeb65d42fc8f4406777920f979cbf Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:13:31 +0100 Subject: [PATCH 06/19] Fix Pandas4Warning: DatetimeIndex.values will change from UTC ndarray to pd.DatetimeArray Pandas4Warning: DatetimeIndex.values returning an ndarray that drops timezone information is deprecated. In a future version, this will return the underlying DatetimeArray instead. Use 'DatetimeIndex.tz_convert(None).to_numpy()' to get a NumPy array of UTC values, or 'DatetimeIndex.array' to get the ExtensionArray. --- pvlib/soiling.py | 4 ++-- pvlib/tools.py | 5 ++++- tests/test_clearsky.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pvlib/soiling.py b/pvlib/soiling.py index dfcd60529e..6fdad506e6 100644 --- a/pvlib/soiling.py +++ b/pvlib/soiling.py @@ -192,9 +192,9 @@ def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015, # convert grace_period to timedelta grace_period = datetime.timedelta(days=grace_period) - # get indices as numpy datetime64, calculate timestep as numpy timedelta64, + # get indices as np.datetime64 (UTC), calculate timestep as np.timedelta64, # and convert timestep to fraction of days - rain_index_vals = rainfall.index.values + rain_index_vals = rainfall.index.tz_convert(None).to_numpy() timestep_interval = (rain_index_vals[1] - rain_index_vals[0]) day_fraction = timestep_interval / np.timedelta64(24, 'h') diff --git a/pvlib/tools.py b/pvlib/tools.py index b519715cb3..3a51f1e2b4 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -428,7 +428,10 @@ def _get_sample_intervals(times, win_length): """ Calculates time interval and samples per window for Reno-style clear sky detection functions """ - deltas = np.diff(times.values) / np.timedelta64(1, '60s') + deltas = ( + np.diff(times.tz_convert(None).to_numpy()) # UTC np.datetime64 + / np.timedelta64(1, '60s') + ) # determine if we can proceed if times.inferred_freq and len(np.unique(deltas)) == 1: diff --git a/tests/test_clearsky.py b/tests/test_clearsky.py index 138cc43f26..332fd70216 100644 --- a/tests/test_clearsky.py +++ b/tests/test_clearsky.py @@ -660,7 +660,7 @@ def test_detect_clearsky_arrays(detect_clearsky_data): def test_detect_clearsky_irregular_times(detect_clearsky_data): expected, cs = detect_clearsky_data - times = cs.index.values.copy() + times = cs.index.tz_convert(None).to_numpy() # UTC np.datetime64 times[0] += pd.Timedelta(1, unit='s') times = pd.DatetimeIndex(times) match = "does not yet support unequal time intervals" From 4045155731a5186aa28b7176e71e8caeae91092c Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:22:45 +0100 Subject: [PATCH 07/19] Fix previous commit - use localize to ignore tz, and not fail when input is tz-naive --- pvlib/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 3a51f1e2b4..aa58b280ad 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -429,7 +429,7 @@ def _get_sample_intervals(times, win_length): sky detection functions """ deltas = ( - np.diff(times.tz_convert(None).to_numpy()) # UTC np.datetime64 + np.diff(times.tz_localize(None).to_numpy()) # tz-naive np.datetime64 / np.timedelta64(1, '60s') ) From 10b9c3032cc6a7928bff7358f4891201384c6d17 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:37:14 +0100 Subject: [PATCH 08/19] Fix prior Pandas4Warning - we need a writable array, not just a ro view --- tests/test_clearsky.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_clearsky.py b/tests/test_clearsky.py index 332fd70216..752d5043b0 100644 --- a/tests/test_clearsky.py +++ b/tests/test_clearsky.py @@ -660,7 +660,10 @@ def test_detect_clearsky_arrays(detect_clearsky_data): def test_detect_clearsky_irregular_times(detect_clearsky_data): expected, cs = detect_clearsky_data - times = cs.index.tz_convert(None).to_numpy() # UTC np.datetime64 + times = (cs.index + .tz_convert(None) # converted to UTC + .to_numpy(copy=True) # np.datetime64 + required writability + ) times[0] += pd.Timedelta(1, unit='s') times = pd.DatetimeIndex(times) match = "does not yet support unequal time intervals" From 3f64bb69c379d36c03eabfb13fb80e4a24cc2560 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:48:33 +0100 Subject: [PATCH 09/19] Fix error in prior commit due to check_freq nonexistent in pandas<3.1.0 --- tests/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index bda29842ae..ce1459fb62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,10 @@ def _check_pandas_assert_kwargs(kwargs): def assert_index_equal(left, right, **kwargs): kwargs = _check_pandas_assert_kwargs(kwargs) - pd.testing.assert_index_equal(left, right, **kwargs, check_freq=False) + try: # check_freq was added in pandas 3.1.0 + pd.testing.assert_index_equal(left, right, **kwargs, check_freq=False) + except TypeError: # this branch supports prior versions + pd.testing.assert_index_equal(left, right, **kwargs) def assert_series_equal(left, right, **kwargs): From bd73b06a4d116a3e17e1c059b61a507d6c19310b Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:49:04 +0100 Subject: [PATCH 10/19] Fix style issues - flake8 --- tests/test_clearsky.py | 3 ++- tests/test_conftest.py | 5 +++-- tests/test_solarposition.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_clearsky.py b/tests/test_clearsky.py index 752d5043b0..20d88679ff 100644 --- a/tests/test_clearsky.py +++ b/tests/test_clearsky.py @@ -660,7 +660,8 @@ def test_detect_clearsky_arrays(detect_clearsky_data): def test_detect_clearsky_irregular_times(detect_clearsky_data): expected, cs = detect_clearsky_data - times = (cs.index + times = ( + cs.index .tz_convert(None) # converted to UTC .to_numpy(copy=True) # np.datetime64 + required writability ) diff --git a/tests/test_conftest.py b/tests/test_conftest.py index dbc9cedaaf..56dfb48519 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -35,8 +35,9 @@ def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, expected_kwargs = {'check_less_precise': check_less_precise} if function_name == "assert_index_equal": # assert_index_equal in Pandas 3.1.0 warns: - # Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs and None do not match. Pass check_freq=True or check_freq=False to silence this warning - # pd.testing.assert_index_equal(left, right, **kwargs) + # Pandas4Warning: assert_index_equal will check the 'freq' attribute + # by default in a future version; these freqs and None do not + # match. Pass check_freq=True or check_freq=False to silence expected_kwargs['check_freq'] = False spy.assert_called_once_with(*args, **expected_kwargs) diff --git a/tests/test_solarposition.py b/tests/test_solarposition.py index ad483eef67..cc7151d5fb 100644 --- a/tests/test_solarposition.py +++ b/tests/test_solarposition.py @@ -815,7 +815,7 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): longitude = golden_mst.longitude eot = solarposition.equation_of_time_spencer71( times_utc.day_of_year) # minutes - decl = solarposition.declination_spencer71(times_utc.day_of_year) # radians + decl = solarposition.declination_spencer71(times_utc.day_of_year) # rad with pytest.raises(ValueError): solarposition.sun_rise_set_transit_geometric( times.tz_convert(None), latitude=latitude, longitude=longitude, From b62991682e48f7928b61b4ff75fb552771d02d6f Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:02:58 +0100 Subject: [PATCH 11/19] Fix check_freq not in pandas<3.1.0; iotools --- tests/iotools/test_solaranywhere.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/iotools/test_solaranywhere.py b/tests/iotools/test_solaranywhere.py index c4ff5858c3..5f4dedfbe1 100644 --- a/tests/iotools/test_solaranywhere.py +++ b/tests/iotools/test_solaranywhere.py @@ -6,6 +6,7 @@ TESTS_DATA_DIR, RERUNS, RERUNS_DELAY, + assert_index_equal, requires_solaranywhere_credentials, ) @@ -74,10 +75,9 @@ def test_read_solaranywhere_high_resolution(high_resolution_index): assert data.loc['2021-01-01 07:00:00-05:00', 'Albedo'] == 0.6 assert data.loc['2021-01-01 07:00:00-05:00', 'WindSpeed (m/s)'] == 0 # Assert that the index is parsed correctly - pd.testing.assert_index_equal( + assert_index_equal( # from conftest; doesn't compare index frequency attr data.index, high_resolution_index, - check_freq=False, ) From 058848caef29b79a196c8913ef882ca94c37890a Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:55:10 +0100 Subject: [PATCH 12/19] Fix check_freq not in pandas<3.1.0; iotools (II) --- tests/iotools/test_tmy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/iotools/test_tmy.py b/tests/iotools/test_tmy.py index 0659c416f6..c95f89986b 100644 --- a/tests/iotools/test_tmy.py +++ b/tests/iotools/test_tmy.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd from pvlib.iotools import tmy -from tests.conftest import TESTS_DATA_DIR, PVLIB_DATA_DIR +from tests.conftest import TESTS_DATA_DIR, PVLIB_DATA_DIR, assert_index_equal import pytest # test the API works @@ -111,10 +111,9 @@ def test_solaranywhere_tmy3(solaranywhere_index): # included to test files with 00:00 timestamps are parsed correctly data, meta = tmy.read_tmy3(TMY3_SOLARANYWHERE, encoding='iso-8859-1', map_variables=False) - pd.testing.assert_index_equal( + assert_index_equal( # from conftest; doesn't compare index frequency attr data.index, solaranywhere_index, - check_freq=False, ) assert meta['USAF'] == 0 assert meta['Name'] == 'Burlington United States' From 6a656c11da8d1b3984cbe3d7a8ee2921da7b5075 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:56:31 +0100 Subject: [PATCH 13/19] Review: tidy check_freq logic --- tests/conftest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ce1459fb62..b58670d194 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,15 +51,16 @@ def _check_pandas_assert_kwargs(kwargs): else: kwargs.pop('rtol', None) kwargs.pop('atol', None) + if Version(pd.__version__) >= Version('3.1.0'): + # check_freq was added in pandas 3.1.0 + # tests written prior to it, may have not taken it into account + kwargs['check_freq'] = False return kwargs def assert_index_equal(left, right, **kwargs): kwargs = _check_pandas_assert_kwargs(kwargs) - try: # check_freq was added in pandas 3.1.0 - pd.testing.assert_index_equal(left, right, **kwargs, check_freq=False) - except TypeError: # this branch supports prior versions - pd.testing.assert_index_equal(left, right, **kwargs) + pd.testing.assert_index_equal(left, right, **kwargs) def assert_series_equal(left, right, **kwargs): From db5f399e38c13fd3c66ac97c34f700f5ed4244b4 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:04:39 +0100 Subject: [PATCH 14/19] Whatsnew --- docs/sphinx/source/whatsnew/v0.16.2.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/whatsnew/v0.16.2.rst b/docs/sphinx/source/whatsnew/v0.16.2.rst index 64132a2547..e4084cf22b 100644 --- a/docs/sphinx/source/whatsnew/v0.16.2.rst +++ b/docs/sphinx/source/whatsnew/v0.16.2.rst @@ -24,6 +24,7 @@ Bug fixes Enhancements ~~~~~~~~~~~~ +* Add ``pandas==3.1.0`` support. (:pull:`2924`) * Add ``dataset`` parameter to :py:func:`~pvlib.iotools.get_era5` allowing for choosing between ERA5 and ERA5-Land datasets. (:pull:`2780`) From d1120386ec4938aa79f6d05aac71d400647c1b3c Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:48:59 +0100 Subject: [PATCH 15/19] Fix _check_pandas_assert_kwargs test (after tidying code) --- tests/test_conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_conftest.py b/tests/test_conftest.py index 56dfb48519..740de33cb5 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -33,7 +33,7 @@ def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, expected_kwargs = {'atol': tol, 'rtol': tol} else: expected_kwargs = {'check_less_precise': check_less_precise} - if function_name == "assert_index_equal": + if function_name == "assert_index_equal" and pd_version == '3.1.0': # assert_index_equal in Pandas 3.1.0 warns: # Pandas4Warning: assert_index_equal will check the 'freq' attribute # by default in a future version; these freqs and None do not From a6ec50d435ae086149ebfff9657e23c351db7bbd Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:35:27 +0100 Subject: [PATCH 16/19] Fix index check_freq implementation and tests check_freq is exclusively supported in assert_index_equal, added in pandas==3.1.0 --- tests/conftest.py | 8 ++++---- tests/test_conftest.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b58670d194..19a44de38f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,15 +51,15 @@ def _check_pandas_assert_kwargs(kwargs): else: kwargs.pop('rtol', None) kwargs.pop('atol', None) - if Version(pd.__version__) >= Version('3.1.0'): - # check_freq was added in pandas 3.1.0 - # tests written prior to it, may have not taken it into account - kwargs['check_freq'] = False return kwargs def assert_index_equal(left, right, **kwargs): kwargs = _check_pandas_assert_kwargs(kwargs) + if Version(pd.__version__) >= Version('3.1.0'): + # check_freq was added in pandas 3.1.0 + # tests written prior to it, may have not taken it into account + kwargs['check_freq'] = False pd.testing.assert_index_equal(left, right, **kwargs) diff --git a/tests/test_conftest.py b/tests/test_conftest.py index 740de33cb5..7a177177ec 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize('function_name', ['assert_index_equal', 'assert_series_equal', 'assert_frame_equal']) -@pytest.mark.parametrize('pd_version', ['1.0.0', '1.1.0']) +@pytest.mark.parametrize('pd_version', ['1.0.0', '1.1.0', '3.1.0']) @pytest.mark.parametrize('check_less_precise', [True, False]) def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, check_less_precise): @@ -28,7 +28,7 @@ def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, assert_function = getattr(conftest, function_name) args = [None, None] assert_function(*args, check_less_precise=check_less_precise) - if pd_version == '1.1.0': + if pd_version in {'1.1.0', '3.1.0'}: tol = 1e-3 if check_less_precise else 1e-5 expected_kwargs = {'atol': tol, 'rtol': tol} else: From 1c6c0c74fcac573ad8784d5ce719987ffc3b9c08 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:51:30 +0100 Subject: [PATCH 17/19] Fix Pandas4Warning - Series.values returning an ndarray that drops timezone information for DatetimeTZDtype is deprecated. tests/test_solarposition.py::test_sun_rise_set_transit_geometric /home/echedey/Documents/10_Programming/pvlib-python/tests/test_solarposition.py:835: Pandas4Warning: Series.values returning an ndarray that drops timezone information for DatetimeTZDtype is deprecated. In a future version, this will return the underlying DatetimeArray instead. Use 'Series.dt.tz_convert(None).to_numpy()' to get a NumPy array of UTC values, or 'Series.array' to get the ExtensionArray. See https://pandas.pydata.org/docs/dev/whatsnew/v3.1.0.html#whatsnew-310-deprecations-values for more details. expected_sunset = pd.DatetimeIndex(expected_rise_set_spa.sunset.values, --- tests/test_solarposition.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_solarposition.py b/tests/test_solarposition.py index cc7151d5fb..fc31094362 100644 --- a/tests/test_solarposition.py +++ b/tests/test_solarposition.py @@ -830,12 +830,15 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): test_sunset = solarposition._times_to_hours_after_local_midnight(ss) test_transit = solarposition._times_to_hours_after_local_midnight(st) # convert expected SPA sunrise, sunset, transit to local datetime indices - expected_sunrise = pd.DatetimeIndex(expected_rise_set_spa.sunrise.values, - tz='UTC').tz_convert(golden_mst.tz) - expected_sunset = pd.DatetimeIndex(expected_rise_set_spa.sunset.values, - tz='UTC').tz_convert(golden_mst.tz) - expected_transit = pd.DatetimeIndex(expected_rise_set_spa.transit.values, - tz='UTC').tz_convert(golden_mst.tz) + expected_sunrise = pd.DatetimeIndex( + expected_rise_set_spa["sunrise"].dt.tz_convert(None).to_numpy(), tz="UTC" + ).tz_convert(golden_mst.tz) + expected_sunset = pd.DatetimeIndex( + expected_rise_set_spa["sunset"].dt.tz_convert(None).to_numpy(), tz="UTC" + ).tz_convert(golden_mst.tz) + expected_transit = pd.DatetimeIndex( + expected_rise_set_spa["transit"].dt.tz_convert(None).to_numpy(), tz="UTC" + ).tz_convert(golden_mst.tz) # convert expected times to hours since midnight as arrays of floats expected_sunrise = solarposition._times_to_hours_after_local_midnight( expected_sunrise) From 8df4f73977d949899c8ce292e7928420b865c8c1 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:46:45 +0100 Subject: [PATCH 18/19] Fix Pandas4Warning: A future version of pandas will return a BaseOffset object instead of a string from infer_freq A future version of pandas will return a BaseOffset object instead of a string from infer_freq. Use pd.set_option('future.infer_freq_returns_offset', True) to get the future behavior, or set to False to keep the old behavior and silence this warning. To preserve the string representation, use ``infer_freq(...).freqstr``. --- pvlib/snow.py | 10 ++++++++-- pvlib/tools.py | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/pvlib/snow.py b/pvlib/snow.py index 4492abc554..85774568bd 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -5,7 +5,13 @@ import numpy as np import pandas as pd -from pvlib.tools import sind, cosd, tand + +from pvlib.tools import ( + _pandas_infer_freq_BaseOffset_transition_handler, + cosd, + sind, + tand, +) def _time_delta_in_hours(times): @@ -58,7 +64,7 @@ def fully_covered_nrel(snowfall, snow_depth=None, threshold_snowfall=1., hourly_snow_rate = snowfall / timestep # if we can infer a time frequency, use first snowfall value # otherwise the first snowfall value is ignored - freq = pd.infer_freq(snowfall.index) + freq = _pandas_infer_freq_BaseOffset_transition_handler(snowfall.index) if freq is not None: timedelta = pd.tseries.frequencies.to_offset(freq) / pd.Timedelta('1h') hourly_snow_rate.iloc[0] = snowfall.iloc[0] / timedelta diff --git a/pvlib/tools.py b/pvlib/tools.py index aa58b280ad..ce92fbd681 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -9,6 +9,7 @@ import numpy as np import pandas as pd +from packaging.version import Version import zoneinfo @@ -434,7 +435,8 @@ def _get_sample_intervals(times, win_length): ) # determine if we can proceed - if times.inferred_freq and len(np.unique(deltas)) == 1: + if (_pandas_infer_freq_BaseOffset_transition_handler(times) + and len(np.unique(deltas)) == 1): sample_interval = times[1] - times[0] sample_interval = sample_interval.seconds / 60 # in minutes samples_per_window = int(win_length / sample_interval) @@ -448,6 +450,41 @@ def _get_sample_intervals(times, win_length): raise NotImplementedError(message) +def _pandas_infer_freq_BaseOffset_transition_handler(times): + """ + Pandas 3.1.0 introduced the following deprecation [^1]: + Pandas4Warning: A future version of pandas will return a BaseOffset + object instead of a string from infer_freq. + Use pd.set_option('future.infer_freq_returns_offset', True) to get the + future behavior, or set to False to keep the old behavior and silence + this warning. + To preserve the string representation, use ``infer_freq(...).freqstr`` + + Side-effects must be avoided in user-code. + + pd.infer_freq documentation is at [^2]. + pd.tseries.offsets.BaseOffset documentation is at [^3]. + + Parameters + ---------- + times : DatetimeIndex | TimedeltaIndex | Series | DatetimeLikeArrayMixin + + Returns + ------- + str + + [^1]: pandas PR GH#64679: https://github.com/pandas-dev/pandas/pull/64679 + [^2]: https://pandas.pydata.org/docs/reference/api/pandas.infer_freq.html + [^3]: https://pandas.pydata.org/docs/reference/api/pandas.tseries.offsets.BaseOffset.html + """ + if Version(pd.__version__) < Version("3.1.0rc0"): + return pd.infer_freq(times) + with pd.option_context({'future.infer_freq_returns_offset': True}): + # relies on BaseOffset to work seamlessly with Pandas 4 + base_offset = pd.infer_freq(times) + # base_offset:=None if unable to infer + return getattr(base_offset, "freqstr", None) + def _degrees_to_index(degrees, coordinate): """Transform input degrees to an output index integer. Specify a degree value and either 'latitude' or 'longitude' to get From 8ccdb20f8cdbdeaf5f750cd781418cd59be74113 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:35:11 +0100 Subject: [PATCH 19/19] Review: Fix Pandas4Warning: assert_index_equal will use check_freq by default tests/iotools/test_era5.py::test_get_era5_land /home/runner/work/pvlib-python/pvlib-python/tests/iotools/test_era5.py:69: Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs None and do not match. Pass check_freq=True or check_freq=False to silence this warning pd.testing.assert_index_equal(df.index, expected.index) tests/iotools/test_nasa_power.py::test_get_nasa_power /home/runner/work/pvlib-python/pvlib-python/tests/iotools/test_nasa_power.py:42: Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs None and do not match. Pass check_freq=True or check_freq=False to silence this warning pd.testing.assert_index_equal(data.index, data_index) tests/iotools/test_nasa_power.py::test_get_nasa_power_pvlib_params_naming /home/runner/work/pvlib-python/pvlib-python/tests/iotools/test_nasa_power.py:57: Pandas4Warning: assert_index_equal will check the 'freq' attribute by default in a future version; these freqs None and do not match. Pass check_freq=True or check_freq=False to silence this warning pd.testing.assert_index_equal(data.index, data_index) --- tests/iotools/test_era5.py | 9 +++++++-- tests/iotools/test_nasa_power.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/iotools/test_era5.py b/tests/iotools/test_era5.py index 6e0703be28..3afab97ffd 100644 --- a/tests/iotools/test_era5.py +++ b/tests/iotools/test_era5.py @@ -8,7 +8,12 @@ import pvlib import requests import os -from tests.conftest import RERUNS, RERUNS_DELAY, requires_ecmwf_credentials +from tests.conftest import ( + RERUNS, + RERUNS_DELAY, + assert_index_equal, + requires_ecmwf_credentials, +) @pytest.fixture @@ -66,7 +71,7 @@ def test_get_era5_land(params, expected): df, meta = pvlib.iotools.get_era5(**params) assert np.isclose(meta['longitude'], -80.0) assert np.isclose(meta['latitude'], 40.0) - pd.testing.assert_index_equal(df.index, expected.index) + assert_index_equal(df.index, expected.index) assert set(df.columns) == {'ghi', 'temp_air'} assert df.notna().all().all() diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index dd09d7b1ca..7d7ffeab32 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -2,7 +2,7 @@ import pytest import pvlib from requests.exceptions import HTTPError -from tests.conftest import RERUNS, RERUNS_DELAY +from tests.conftest import RERUNS, RERUNS_DELAY, assert_index_equal @pytest.fixture @@ -39,7 +39,7 @@ def test_get_nasa_power(data_index, ghi_series): assert meta['time_standard'] == 'UTC' assert meta['title'] == 'NASA/POWER Source Native Resolution Hourly Data' # Assert that the index is parsed correctly - pd.testing.assert_index_equal(data.index, data_index) + assert_index_equal(data.index, data_index) # Test one column pd.testing.assert_series_equal(data['ALLSKY_SFC_SW_DWN'], ghi_series, check_freq=False, check_names=False) @@ -54,7 +54,7 @@ def test_get_nasa_power_pvlib_params_naming(data_index, ghi_series): end=data_index[-1], parameters=['ghi']) # Assert that the index is parsed correctly - pd.testing.assert_index_equal(data.index, data_index) + assert_index_equal(data.index, data_index) # Test one column pd.testing.assert_series_equal(data['ghi'], ghi_series, check_freq=False)