Skip to content

Commit c01a23a

Browse files
authored
minor: Support pandas 3.0 and numpy 2.4 (#2715)
1 parent 2cdb2cb commit c01a23a

8 files changed

Lines changed: 87 additions & 31 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,44 @@ Using `cmdstanpy` with Windows requires a Unix-compatible C compiler such as min
129129

130130
See [Release Notes](https://github.com/facebook/prophet/releases).
131131

132+
### Version 1.3.0 (2026.01.27)
133+
134+
#### Python
135+
136+
- Support pandas>=3.0 and numpy>=2.4.
137+
138+
### Version 1.2.2 (2026.01.25)
139+
140+
#### Python
141+
142+
- Version constraints on pandas (`<3`) and numpy (`<2.4`).
143+
144+
#### R
145+
- Update build requirements to C++17 to Comply with CRAN Policy.
146+
- Add .tar.gz upload for R package to CI.
147+
- Re-generated holidays.csv for R package.
148+
149+
### Version 1.2.1 (2025.10.22)
150+
151+
#### Python
152+
153+
- Also copy makefile to fake cmdstan.
154+
155+
### Version 1.2.0 (2025.05.30)
156+
157+
#### Python
158+
159+
- Use latest CmdStan.
160+
- Add null check to CmdStanPyBackend cleanup() function.
161+
162+
### Version 1.1.7 (2025.05.30)
163+
164+
#### Python
165+
166+
- Enable creation of custom performance metrics.
167+
- chore: address pandas futurewarning from "M" being deprecated.
168+
- cleanup() for cross_validate.
169+
132170
### Version 1.1.6 (2024.09.29)
133171

134172
#### Python

python/prophet/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.2.2"
1+
__version__ = "1.3.0"

python/prophet/diagnostics.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
from tqdm.auto import tqdm
1111
from copy import deepcopy
1212
import concurrent.futures
13+
import multiprocessing
14+
import sys
1315

1416
import numpy as np
1517
import pandas as pd
1618

19+
1720
logger = logging.getLogger('prophet')
1821

1922

@@ -107,7 +110,7 @@ def map(self, func, *iterables):
107110
for args in zip(*iterables)
108111
]
109112
return results
110-
113+
111114
disable_tqdm: if True it disables the progress bar that would otherwise show up when parallel=None
112115
extra_output_columns: A String or List of Strings e.g. 'trend' or ['trend'].
113116
Additional columns to 'yhat' and 'ds' to be returned in output.
@@ -116,27 +119,27 @@ def map(self, func, *iterables):
116119
-------
117120
A pd.DataFrame with the forecast, actual value and cutoff.
118121
"""
119-
122+
120123
if model.history is None:
121124
raise Exception('Model has not been fit. Fitting the model provides contextual parameters for cross validation.')
122-
125+
123126
df = model.history.copy().reset_index(drop=True)
124127
horizon = pd.Timedelta(horizon)
125128
predict_columns = ['ds', 'yhat']
126-
129+
127130
if model.uncertainty_samples:
128131
predict_columns.extend(['yhat_lower', 'yhat_upper'])
129132

130133
if extra_output_columns is not None:
131134
if isinstance(extra_output_columns, str):
132135
extra_output_columns = [extra_output_columns]
133136
predict_columns.extend([c for c in extra_output_columns if c not in predict_columns])
134-
137+
135138
# Identify the largest seasonality period
136139
period_max = 0.
137140
for s in model.seasonalities.values():
138141
period_max = max(period_max, s['period'])
139-
seasonality_dt = pd.Timedelta(str(period_max) + ' days')
142+
seasonality_dt = pd.Timedelta(str(period_max) + ' days')
140143

141144
if cutoffs is None:
142145
# Set period
@@ -152,15 +155,15 @@ def map(self, func, *iterables):
152155
cutoffs = generate_cutoffs(df, horizon, initial, period)
153156
else:
154157
# add validation of the cutoff to make sure that the min cutoff is strictly greater than the min date in the history
155-
if min(cutoffs) <= df['ds'].min():
158+
if min(cutoffs) <= df['ds'].min():
156159
raise ValueError("Minimum cutoff value is not strictly greater than min date in history")
157160
# max value of cutoffs is <= (end date minus horizon)
158-
end_date_minus_horizon = df['ds'].max() - horizon
159-
if max(cutoffs) > end_date_minus_horizon:
161+
end_date_minus_horizon = df['ds'].max() - horizon
162+
if max(cutoffs) > end_date_minus_horizon:
160163
raise ValueError("Maximum cutoff value is greater than end date minus horizon, no value for cross-validation remaining")
161164
initial = cutoffs[0] - df['ds'].min()
162-
163-
# Check if the initial window
165+
166+
# Check if the initial window
164167
# (that is, the amount of time between the start of the history and the first cutoff)
165168
# is less than the maximum seasonality period
166169
if initial < seasonality_dt:
@@ -175,7 +178,11 @@ def map(self, func, *iterables):
175178
if parallel == "threads":
176179
pool = concurrent.futures.ThreadPoolExecutor()
177180
elif parallel == "processes":
178-
pool = concurrent.futures.ProcessPoolExecutor()
181+
if sys.platform.startswith("win") or sys.platform == "darwin":
182+
ctx = multiprocessing.get_context("spawn")
183+
else:
184+
ctx = multiprocessing.get_context("forkserver")
185+
pool = concurrent.futures.ProcessPoolExecutor(mp_context=ctx)
179186
elif parallel == "dask":
180187
try:
181188
from dask.distributed import get_client
@@ -204,7 +211,7 @@ def map(self, func, *iterables):
204211

205212
else:
206213
predicts = [
207-
single_cutoff_forecast(df, model, cutoff, horizon, predict_columns)
214+
single_cutoff_forecast(df, model, cutoff, horizon, predict_columns)
208215
for cutoff in (tqdm(cutoffs) if not disable_tqdm else cutoffs)
209216
]
210217

@@ -334,7 +341,7 @@ def register_performance_metric(func):
334341
df: Cross-validation results dataframe.
335342
w: Aggregation window size.
336343
337-
Registered metric should return following
344+
Registered metric should return following
338345
-------
339346
Dataframe with columns horizon and metric.
340347
"""
@@ -382,7 +389,7 @@ def performance_metrics(df, metrics=None, rolling_window=0.1, monthly=False):
382389
use ['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape', 'coverage'].
383390
rolling_window: Proportion of data to use in each rolling window for
384391
computing the metrics. Should be in [0, 1] to average.
385-
monthly: monthly=True will compute horizons as numbers of calendar months
392+
monthly: monthly=True will compute horizons as numbers of calendar months
386393
from the cutoff date, starting from 0 for the cutoff month.
387394
388395
Returns
@@ -477,7 +484,7 @@ def rolling_mean_by_h(x, h, w, name):
477484
res_x = res_x[(trailing_i + 1):]
478485

479486
return pd.DataFrame({'horizon': res_h, name: res_x})
480-
487+
481488

482489

483490
def rolling_median_by_h(x, h, w, name):

python/prophet/forecaster.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
class Prophet(object):
2929
stan_backend: IStanBackend
30-
30+
3131
"""Prophet forecaster.
3232
3333
Parameters
@@ -476,8 +476,8 @@ def fourier_series(
476476
if not (series_order >= 1):
477477
raise ValueError("series_order must be >= 1")
478478

479-
# convert to days since epoch
480-
t = dates.to_numpy(dtype=np.int64) // NANOSECONDS_TO_SECONDS / (3600 * 24.)
479+
epoch = pd.Timestamp("1970-01-01", tz=dates.dt.tz)
480+
t = (dates - epoch).dt.total_seconds() / (24 * 60 * 60)
481481

482482
x_T = t * np.pi * 2
483483
fourier_components = np.empty((dates.shape[0], 2 * series_order))
@@ -936,7 +936,7 @@ def add_group_component(self, components, name, group):
936936
group_cols = new_comp['col'].unique()
937937
if len(group_cols) > 0:
938938
new_comp = pd.DataFrame({'col': group_cols, 'component': name})
939-
components = pd.concat([components, new_comp])
939+
components = pd.concat([components, new_comp], ignore_index=True)
940940
return components
941941

942942
def parse_seasonality_args(self, name, arg, auto_disable, default_order):
@@ -1332,16 +1332,19 @@ def piecewise_logistic(t, cap, deltas, k, m, changepoint_ts):
13321332
Vector y(t).
13331333
"""
13341334
# Compute offset changes
1335-
k_cum = np.concatenate((np.atleast_1d(k), np.cumsum(deltas) + k))
1335+
# Ensure k and m are scalars for numpy 2.x compatibility
1336+
k_scalar = np.asarray(k).item() if np.asarray(k).size == 1 else k
1337+
m_scalar = np.asarray(m).item() if np.asarray(m).size == 1 else m
1338+
k_cum = np.concatenate((np.atleast_1d(k_scalar), np.cumsum(deltas) + k_scalar))
13361339
gammas = np.zeros(len(changepoint_ts))
13371340
for i, t_s in enumerate(changepoint_ts):
13381341
gammas[i] = (
1339-
(t_s - m - np.sum(gammas))
1342+
(t_s - m_scalar - np.sum(gammas))
13401343
* (1 - k_cum[i] / k_cum[i + 1]) # noqa W503
13411344
)
13421345
# Get cumulative rate and offset at each t
1343-
k_t = k * np.ones_like(t)
1344-
m_t = m * np.ones_like(t)
1346+
k_t = k_scalar * np.ones_like(t)
1347+
m_t = m_scalar * np.ones_like(t)
13451348
for s, t_s in enumerate(changepoint_ts):
13461349
indx = t >= t_s
13471350
k_t[indx] += deltas[s]

python/prophet/serialize.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ def model_from_dict(model_dict):
169169
s = s.dt.tz_localize(None)
170170
setattr(model, attribute, s)
171171
for attribute in PD_TIMESTAMP:
172-
setattr(model, attribute, pd.Timestamp.utcfromtimestamp(model_dict[attribute]).tz_localize(None))
172+
pd_ts = pd.Timestamp.fromtimestamp(model_dict[attribute], tz="UTC").tz_localize(None)
173+
setattr(model, attribute, pd_ts)
173174
for attribute in PD_TIMEDELTA:
174175
setattr(model, attribute, pd.Timedelta(seconds=model_dict[attribute]))
175176
for attribute in PD_DATAFRAME:

python/prophet/tests/test_prophet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ def test_flat_growth(self, backend, scaling):
305305
x = np.linspace(0, 2 * np.pi, 8 * 7)
306306
history = pd.DataFrame(
307307
{
308-
"ds": pd.date_range(start="2020-01-01", periods=8 * 7, freq="d"),
308+
"ds": pd.date_range(start="2020-01-01", periods=8 * 7, freq="D"),
309309
"y": 30 + np.sin(x * 8.0),
310310
}
311311
)

python/prophet/tests/test_serialize.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ def test_simple_serialize(self, daily_univariate_ts, backend):
4040
elif k in PD_SERIES and v is not None:
4141
assert v.equals(m2.__dict__[k])
4242
elif k in PD_DATAFRAME and v is not None:
43-
pd.testing.assert_frame_equal(v, m2.__dict__[k], check_index_type=False)
43+
# check_dtype=False since .fit() and .predict() will cooerce to the correct types
44+
pd.testing.assert_frame_equal(
45+
v, m2.__dict__[k], check_index_type=False, check_dtype=False
46+
)
4447
elif k == "changepoints_t":
4548
assert np.array_equal(v, m.__dict__[k])
4649
else:
@@ -111,7 +114,10 @@ def test_full_serialize(self, daily_univariate_ts, backend):
111114
elif k in PD_SERIES and v is not None:
112115
assert v.equals(m2.__dict__[k])
113116
elif k in PD_DATAFRAME and v is not None:
114-
pd.testing.assert_frame_equal(v, m2.__dict__[k], check_index_type=False)
117+
# check_dtype=False since .fit() and .predict() will cooerce to the correct types
118+
pd.testing.assert_frame_equal(
119+
v, m2.__dict__[k], check_index_type=False, check_dtype=False
120+
)
115121
elif k == "changepoints_t":
116122
assert np.array_equal(v, m.__dict__[k])
117123
else:

python/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ readme = "README.md"
1414
requires-python = ">=3.7"
1515
dependencies = [
1616
"cmdstanpy>=1.0.4",
17-
"numpy>=1.15.4,<2.4.0",
17+
"numpy>=1.15.4",
1818
"matplotlib>=2.0.0",
19-
"pandas>=1.0.4,<3",
19+
"pandas>=1.0.4",
2020
"holidays>=0.25,<1",
2121
"tqdm>=4.36.1",
2222
"importlib_resources",
@@ -37,6 +37,7 @@ classifiers = [
3737
"Programming Language :: Python :: 3.9",
3838
"Programming Language :: Python :: 3.10",
3939
"Programming Language :: Python :: 3.11",
40+
"Programming Language :: Python :: 3.12",
4041
]
4142

4243
[project.optional-dependencies]

0 commit comments

Comments
 (0)