Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
630e584
Fix deprecated inplace operations
echedey-ls Oct 23, 2026
889bcb1
Fix Pandas4Warning: .dayofyear -> .day_of_year
echedey-ls Oct 24, 2026
9f3ae9b
Fix Pandas4Warning: deprecated passing a list to DataFrame.from_dict …
echedey-ls Oct 24, 2026
279dec7
Fix Pandas4Warning: assert_index_equal will check the 'freq' attribute
echedey-ls Oct 24, 2026
b2aba02
Fix pandas 3.1.0 unexpected behavior on .map (may defer until pandas …
echedey-ls Oct 25, 2026
65371c8
Fix Pandas4Warning: DatetimeIndex.values will change from UTC ndarray…
echedey-ls Oct 25, 2026
4045155
Fix previous commit - use localize to ignore tz, and not fail when in…
echedey-ls Oct 25, 2026
10b9c30
Fix prior Pandas4Warning - we need a writable array, not just a ro view
echedey-ls Oct 25, 2026
3f64bb6
Fix error in prior commit due to check_freq nonexistent in pandas<3.1.0
echedey-ls Oct 25, 2026
bd73b06
Fix style issues - flake8
echedey-ls Oct 25, 2026
b629916
Fix check_freq not in pandas<3.1.0; iotools
echedey-ls Oct 25, 2026
058848c
Fix check_freq not in pandas<3.1.0; iotools (II)
echedey-ls Oct 25, 2026
6a656c1
Review: tidy check_freq logic
echedey-ls Oct 26, 2026
db5f399
Whatsnew
echedey-ls Oct 26, 2026
7380eab
Merge branch 'main' into dev/breaking-news-pandas3.1
echedey-ls Oct 26, 2026
d112038
Fix _check_pandas_assert_kwargs test (after tidying code)
echedey-ls Oct 27, 2026
a6ec50d
Fix index check_freq implementation and tests
echedey-ls Oct 27, 2026
1c6c0c7
Fix Pandas4Warning - Series.values returning an ndarray that drops ti…
echedey-ls Oct 27, 2026
8df4f73
Fix Pandas4Warning: A future version of pandas will return a BaseOffs…
echedey-ls Oct 27, 2026
8ccdb20
Review: Fix Pandas4Warning: assert_index_equal will use check_freq by…
echedey-ls Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/benchmarks/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 6 additions & 6 deletions docs/examples/solar-position/plot_sunpath_diagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/sphinx/source/whatsnew/v0.16.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
2 changes: 1 addition & 1 deletion pvlib/clearsky.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pvlib/iotools/pvgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 2 additions & 3 deletions pvlib/iotools/solargis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions pvlib/iotools/solcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -474,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:
Expand Down
2 changes: 1 addition & 1 deletion pvlib/iotools/solrad.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
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"

Check failure on line 201 in pvlib/iotools/solrad.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E231 missing whitespace after ':'
for d in dates
]

Expand Down
4 changes: 2 additions & 2 deletions pvlib/iotools/surfrad.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
]

Expand Down
2 changes: 1 addition & 1 deletion pvlib/iotools/tmy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 6 additions & 6 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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°.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
-------
Expand Down
2 changes: 1 addition & 1 deletion pvlib/ivtools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion pvlib/snow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pvlib/soiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
2 changes: 1 addition & 1 deletion pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions pvlib/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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_localize(None).to_numpy()) # tz-naive np.datetime64
/ np.timedelta64(1, '60s')
)

# determine if we can proceed
if times.inferred_freq and len(np.unique(deltas)) == 1:
Expand Down
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def _check_pandas_assert_kwargs(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)


Expand Down
6 changes: 5 additions & 1 deletion tests/iotools/test_solaranywhere.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TESTS_DATA_DIR,
RERUNS,
RERUNS_DELAY,
assert_index_equal,
requires_solaranywhere_credentials,
)

Expand Down Expand Up @@ -74,7 +75,10 @@ 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)
assert_index_equal( # from conftest; doesn't compare index frequency attr
data.index,
high_resolution_index,
)


def test_read_solaranywhere_map_variables():
Expand Down
20 changes: 10 additions & 10 deletions tests/iotools/test_solcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]])
)
)
Expand All @@ -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]])
)

Expand Down Expand Up @@ -93,15 +93,15 @@ 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]]
)
),
)
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]]
),
)
Expand Down Expand Up @@ -145,15 +145,15 @@ 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]]
)
),
)
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]]
),
)
Expand Down Expand Up @@ -232,15 +232,15 @@ 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]]
)
),
)
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]]
),
)
Expand Down Expand Up @@ -288,15 +288,15 @@ 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]]
)
),
)
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]]
),
)
Expand Down
7 changes: 5 additions & 2 deletions tests/iotools/test_tmy.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -111,7 +111,10 @@ 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)
assert_index_equal( # from conftest; doesn't compare index frequency attr
data.index,
solaranywhere_index,
)
assert meta['USAF'] == 0
assert meta['Name'] == 'Burlington United States'
assert meta['State'] == 'NA'
Expand Down
Loading
Loading