rdtools 3.1.1__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rdtools-3.1.1/rdtools.egg-info → rdtools-3.2.0}/PKG-INFO +1 -1
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/_version.py +3 -3
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/analysis_chains.py +14 -5
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/degradation.py +136 -23
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/plotting.py +80 -12
- {rdtools-3.1.1 → rdtools-3.2.0/rdtools.egg-info}/PKG-INFO +1 -1
- {rdtools-3.1.1 → rdtools-3.2.0}/LICENSE +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/MANIFEST.in +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/README.md +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/__init__.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/_deprecation.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/aggregation.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/availability.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/bootstrap.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/clearsky_temperature.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/data/temperature.hdf5 +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/filtering.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/models/xgboost_clipping_model.json +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/normalization.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/soiling.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/utilities.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/SOURCES.txt +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/dependency_links.txt +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/not-zip-safe +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/requires.txt +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/top_level.txt +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/setup.cfg +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/setup.py +0 -0
- {rdtools-3.1.1 → rdtools-3.2.0}/versioneer.py +0 -0
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2026-
|
|
11
|
+
"date": "2026-07-01T15:51:37-0400",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "3.
|
|
14
|
+
"full-revisionid": "5fe5e7e4fe612274c51c1fbe6dd419e8a6df5870",
|
|
15
|
+
"version": "3.2.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -1053,7 +1053,7 @@ class TrendAnalysis:
|
|
|
1053
1053
|
Analyses to perform as a list of strings. Valid entries are 'yoy_degradation'
|
|
1054
1054
|
and 'srr_soiling'
|
|
1055
1055
|
yoy_kwargs : dict
|
|
1056
|
-
kwargs to pass to :py:func:`rdtools.degradation.degradation_year_on_year
|
|
1056
|
+
kwargs to pass to :py:func:`rdtools.degradation.degradation_year_on_year`.
|
|
1057
1057
|
srr_kwargs : dict
|
|
1058
1058
|
kwargs to pass to :py:func:`rdtools.soiling.soiling_srr`
|
|
1059
1059
|
|
|
@@ -1248,7 +1248,7 @@ class TrendAnalysis:
|
|
|
1248
1248
|
ax.set_ylabel("PV Energy (Wh/timestep)")
|
|
1249
1249
|
return fig
|
|
1250
1250
|
|
|
1251
|
-
def plot_degradation_timeseries(self, case, rolling_days=365, **kwargs):
|
|
1251
|
+
def plot_degradation_timeseries(self, case, rolling_days=365, center=None, **kwargs):
|
|
1252
1252
|
"""
|
|
1253
1253
|
Plot resampled time series of degradation trend with time
|
|
1254
1254
|
|
|
@@ -1257,8 +1257,17 @@ class TrendAnalysis:
|
|
|
1257
1257
|
case: str
|
|
1258
1258
|
The workflow result to plot, allowed values are 'sensor' and 'clearsky'
|
|
1259
1259
|
rolling_days: int, default 365
|
|
1260
|
-
Number of days for rolling window.
|
|
1261
|
-
|
|
1260
|
+
Number of days for rolling window. The window must contain at least
|
|
1261
|
+
``rolling_days // min_periods_divisor`` datapoints to be included in
|
|
1262
|
+
the rolling plot; see
|
|
1263
|
+
:py:func:`rdtools.plotting.degradation_timeseries_plot` for details
|
|
1264
|
+
on ``min_periods_divisor`` and its pending default change.
|
|
1265
|
+
center : bool, default False
|
|
1266
|
+
If ``True``, the rolling window is centered and results are reindexed
|
|
1267
|
+
using center timestamps before any calculations are performed.
|
|
1268
|
+
The recommended value is ``True``; the default of ``False`` is retained
|
|
1269
|
+
only for backward compatibility. A warning is raised when this argument
|
|
1270
|
+
is not explicitly supplied.
|
|
1262
1271
|
kwargs :
|
|
1263
1272
|
Extra parameters passed to :py:func:`rdtools.plotting.degradation_timeseries_plot`
|
|
1264
1273
|
|
|
@@ -1274,7 +1283,7 @@ class TrendAnalysis:
|
|
|
1274
1283
|
else:
|
|
1275
1284
|
raise ValueError("case must be either 'sensor' or 'clearsky'")
|
|
1276
1285
|
|
|
1277
|
-
fig = plotting.degradation_timeseries_plot(yoy_info, rolling_days, **kwargs)
|
|
1286
|
+
fig = plotting.degradation_timeseries_plot(yoy_info, rolling_days, center=center, **kwargs)
|
|
1278
1287
|
return fig
|
|
1279
1288
|
|
|
1280
1289
|
|
|
@@ -179,7 +179,8 @@ def degradation_classical_decomposition(energy_normalized,
|
|
|
179
179
|
|
|
180
180
|
def degradation_year_on_year(energy_normalized, recenter=True,
|
|
181
181
|
exceedance_prob=95, confidence_level=68.2,
|
|
182
|
-
uncertainty_method='simple', block_length=30
|
|
182
|
+
uncertainty_method='simple', block_length=30,
|
|
183
|
+
multi_yoy=False):
|
|
183
184
|
'''
|
|
184
185
|
Estimate the trend of a timeseries using the year-on-year decomposition
|
|
185
186
|
approach and calculate a Monte Carlo-derived confidence interval of slope.
|
|
@@ -208,6 +209,11 @@ def degradation_year_on_year(energy_normalized, recenter=True,
|
|
|
208
209
|
If `uncertainty_method` is 'circular_block', `block_length`
|
|
209
210
|
determines the length of the blocks used in the circular block bootstrapping
|
|
210
211
|
in number of days. Must be shorter than a third of the time series.
|
|
212
|
+
multi_yoy : bool, default False
|
|
213
|
+
Whether to return the standard Year-on-Year slopes where each slope
|
|
214
|
+
is calculated over points separated by 365 days (default) or
|
|
215
|
+
multi_year-on-year where points can be separated by N * 365 days
|
|
216
|
+
where N is an integer from 1 to the length of the dataset in years.
|
|
211
217
|
|
|
212
218
|
Returns
|
|
213
219
|
-------
|
|
@@ -218,14 +224,24 @@ def degradation_year_on_year(energy_normalized, recenter=True,
|
|
|
218
224
|
degradation rate estimate
|
|
219
225
|
calc_info : dict
|
|
220
226
|
|
|
221
|
-
* `YoY_values` - pandas series of
|
|
227
|
+
* `YoY_values` - pandas series of year on year slopes with integer index.
|
|
228
|
+
When ``multi_yoy=True`` the index is non-monotonic because multiple
|
|
229
|
+
overlapping annual slopes can share the same right-endpoint position.
|
|
222
230
|
* `renormalizing_factor` - float of value used to recenter data
|
|
223
231
|
* `exceedance_level` - the degradation rate that was outperformed with
|
|
224
232
|
probability of `exceedance_prob`
|
|
225
233
|
* `usage_of_points` - number of times each point in energy_normalized
|
|
226
234
|
is used to calculate a degradation slope. 0: point is never used. 1:
|
|
227
235
|
point is either used as a start or endpoint. 2: point is used as both
|
|
228
|
-
start and endpoint for an Rd calculation.
|
|
236
|
+
start and endpoint for an Rd calculation. With ``multi_yoy=True``,
|
|
237
|
+
values can be larger than 2 because each point participates in
|
|
238
|
+
multiple slopes.
|
|
239
|
+
* `YoY_times` - pandas DataFrame with columns ``dt_right``, ``dt_center``,
|
|
240
|
+
and ``dt_left`` giving, for each entry in ``YoY_values``, the
|
|
241
|
+
timestamps of the right endpoint, the midpoint, and the left endpoint
|
|
242
|
+
of the slope. This can be used to recover the original timestamp-
|
|
243
|
+
indexed behavior of ``YoY_values`` (for example,
|
|
244
|
+
``calc_info['YoY_values'].set_axis(calc_info['YoY_times']['dt_right'])``).
|
|
229
245
|
'''
|
|
230
246
|
|
|
231
247
|
# Ensure the data is in order
|
|
@@ -269,37 +285,72 @@ def degradation_year_on_year(energy_normalized, recenter=True,
|
|
|
269
285
|
energy_normalized = energy_normalized.reset_index()
|
|
270
286
|
energy_normalized['energy'] = energy_normalized['energy'] / renorm
|
|
271
287
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
288
|
+
# dataframe container for combined year-over-year changes
|
|
289
|
+
df = pd.DataFrame()
|
|
290
|
+
if multi_yoy:
|
|
291
|
+
year_range = range(1, int((energy_normalized.iloc[-1]['dt'] -
|
|
292
|
+
energy_normalized.iloc[0]['dt']).days/365)+1)
|
|
293
|
+
else:
|
|
294
|
+
year_range = [1]
|
|
295
|
+
for y in year_range:
|
|
296
|
+
energy_normalized['dt_shifted'] = energy_normalized.dt + pd.DateOffset(years=y)
|
|
297
|
+
# Merge with what happened one year ago, use tolerance of 8 days to allow
|
|
298
|
+
# for weekly aggregated data
|
|
299
|
+
df_temp = pd.merge_asof(energy_normalized[['dt', 'energy']],
|
|
300
|
+
energy_normalized.sort_values('dt_shifted'),
|
|
301
|
+
left_on='dt', right_on='dt_shifted',
|
|
302
|
+
suffixes=['', '_left'],
|
|
303
|
+
tolerance=pd.Timedelta('8D')
|
|
304
|
+
)
|
|
305
|
+
df = pd.concat([df, df_temp], ignore_index=True)
|
|
306
|
+
|
|
307
|
+
df['time_diff_years'] = (df.dt - df.dt_left) / pd.Timedelta('365D')
|
|
308
|
+
df['yoy'] = 100.0 * (df.energy - df.energy_left) / (df.time_diff_years)
|
|
286
309
|
|
|
287
310
|
yoy_result = df.yoy.dropna()
|
|
288
311
|
|
|
289
|
-
df_right = df.set_index(df.dt_right).drop_duplicates('dt_right')
|
|
290
|
-
df['usage_of_points'] = df.yoy.notnull().astype(int).add(
|
|
291
|
-
df_right.yoy.notnull().astype(int), fill_value=0)
|
|
292
|
-
|
|
293
312
|
if not len(yoy_result):
|
|
294
313
|
raise ValueError('no year-over-year aggregated data pairs found')
|
|
295
314
|
|
|
296
315
|
Rd_pct = yoy_result.median()
|
|
297
316
|
|
|
317
|
+
YoY_times = df.dropna(subset=['yoy'], inplace=False).copy()
|
|
318
|
+
|
|
319
|
+
# calculate usage of points.
|
|
320
|
+
df_left = YoY_times.set_index(YoY_times.dt_left) # .drop_duplicates('dt_left')
|
|
321
|
+
df_right = YoY_times.set_index(YoY_times.dt) # .drop_duplicates('dt')
|
|
322
|
+
usage_of_points = df_right.yoy.notnull().astype(int).add(
|
|
323
|
+
df_left.yoy.notnull().astype(int),
|
|
324
|
+
fill_value=0).groupby(level=0).sum()
|
|
325
|
+
usage_of_points.name = 'usage_of_points'
|
|
326
|
+
|
|
327
|
+
pandas_version = pd.__version__.split(".")
|
|
328
|
+
if int(pandas_version[0]) < 2:
|
|
329
|
+
# For old Pandas versions < 2.0.0, time columns cannot be averaged
|
|
330
|
+
# with each other, so we use a custom function to calculate center label
|
|
331
|
+
YoY_times['dt_center'] = _avg_timestamp_old_Pandas(YoY_times['dt'], YoY_times['dt_left'])
|
|
332
|
+
else:
|
|
333
|
+
YoY_times['dt_center'] = pd.to_datetime(YoY_times[['dt', 'dt_left']].mean(axis=1))
|
|
334
|
+
|
|
335
|
+
YoY_times = YoY_times[['dt', 'dt_center', 'dt_left']]
|
|
336
|
+
YoY_times = YoY_times.rename(columns={'dt': 'dt_right'})
|
|
337
|
+
|
|
338
|
+
# apply integer index to the yoy_result; multi-YoY has duplicate timestamps.
|
|
339
|
+
yoy_result.index = YoY_times.index
|
|
340
|
+
yoy_result.index.name = 'dt'
|
|
341
|
+
|
|
342
|
+
# the following is throwing a futurewarning if infer_objects() isn't included here.
|
|
343
|
+
# see https://github.com/pandas-dev/pandas/issues/57734
|
|
344
|
+
energy_normalized = energy_normalized.merge(usage_of_points, how='left', left_on='dt',
|
|
345
|
+
right_index=True, left_index=False
|
|
346
|
+
).infer_objects().fillna(0.0)
|
|
347
|
+
|
|
298
348
|
if uncertainty_method == 'simple': # If we need the full results
|
|
299
349
|
calc_info = {
|
|
300
350
|
'YoY_values': yoy_result,
|
|
301
351
|
'renormalizing_factor': renorm,
|
|
302
|
-
'usage_of_points':
|
|
352
|
+
'usage_of_points': energy_normalized.set_index('dt')['usage_of_points'],
|
|
353
|
+
'YoY_times': YoY_times[['dt_right', 'dt_center', 'dt_left']]
|
|
303
354
|
}
|
|
304
355
|
|
|
305
356
|
# bootstrap to determine 68% CI and exceedance probability
|
|
@@ -345,17 +396,79 @@ def degradation_year_on_year(energy_normalized, recenter=True,
|
|
|
345
396
|
|
|
346
397
|
# Save calculation information
|
|
347
398
|
calc_info = {
|
|
399
|
+
'YoY_values': yoy_result,
|
|
348
400
|
'renormalizing_factor': renorm,
|
|
349
401
|
'exceedance_level': exceedance_level,
|
|
350
|
-
'usage_of_points':
|
|
402
|
+
'usage_of_points': energy_normalized.set_index('dt')['usage_of_points'],
|
|
403
|
+
'YoY_times': YoY_times[['dt_right', 'dt_center', 'dt_left']],
|
|
351
404
|
'bootstrap_rates': bootstrap_rates}
|
|
352
405
|
|
|
353
406
|
return (Rd_pct, Rd_CI, calc_info)
|
|
354
407
|
|
|
355
408
|
else: # If we do not need confidence intervals and exceedance level
|
|
409
|
+
# TODO: Consider returning a tuple for consistency with other branches, e.g.:
|
|
410
|
+
# return (Rd_pct, None, {
|
|
411
|
+
# 'YoY_values': yoy_result,
|
|
412
|
+
# 'usage_of_points': energy_normalized.set_index('dt')['usage_of_points'],
|
|
413
|
+
# 'YoY_times': YoY_times[['dt_right', 'dt_center', 'dt_left']]}
|
|
414
|
+
# )
|
|
415
|
+
# Note: Current behavior intentionally returns only the scalar Rd_pct
|
|
416
|
+
# to maintain compatibility (see test_bootstrap_module).
|
|
356
417
|
return Rd_pct
|
|
357
418
|
|
|
358
419
|
|
|
420
|
+
def _avg_timestamp_old_Pandas(dt, dt_left):
|
|
421
|
+
'''
|
|
422
|
+
For old Pandas versions < 2.0.0, time columns cannot be averaged
|
|
423
|
+
together. From https://stackoverflow.com/questions/57812300/
|
|
424
|
+
python-pandas-to-calculate-mean-of-datetime-of-multiple-columns
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
dt : pandas.Series
|
|
429
|
+
First series with datetime values
|
|
430
|
+
dt_left : pandas.Series
|
|
431
|
+
Second series with datetime values.
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
pandas.Series
|
|
436
|
+
Series with the average timestamp of df1 and df2.
|
|
437
|
+
'''
|
|
438
|
+
import calendar
|
|
439
|
+
|
|
440
|
+
# Remove timezone from datetime values for averaging
|
|
441
|
+
temp_df = pd.DataFrame(
|
|
442
|
+
{"dt": dt.dt.tz_localize(None), "dt_left": dt_left.dt.tz_localize(None)}
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# conversion from dates to seconds since epoch (unix time)
|
|
446
|
+
def to_unix(s):
|
|
447
|
+
if isinstance(s, pd.Timestamp):
|
|
448
|
+
return calendar.timegm(s.timetuple())
|
|
449
|
+
else:
|
|
450
|
+
return pd.NaT
|
|
451
|
+
|
|
452
|
+
# sum the seconds since epoch, calculate average, and convert back to readable date
|
|
453
|
+
averages = []
|
|
454
|
+
for index, row in temp_df.iterrows():
|
|
455
|
+
unix = [to_unix(i) for i in row]
|
|
456
|
+
# unix = [pd.Timestamp(i).timestamp() for i in row]
|
|
457
|
+
try:
|
|
458
|
+
average = sum(unix) / len(unix)
|
|
459
|
+
# averages.append(datetime.datetime.utcfromtimestamp(average).strftime('%Y-%m-%d'))
|
|
460
|
+
averages.append(pd.to_datetime(average, unit='s'))
|
|
461
|
+
except TypeError:
|
|
462
|
+
averages.append(pd.NaT)
|
|
463
|
+
temp_df['averages'] = averages
|
|
464
|
+
|
|
465
|
+
dt_center = temp_df["averages"].dt.tz_localize(dt.dt.tz)
|
|
466
|
+
dt_center.index = dt.index
|
|
467
|
+
dt_center.name = "averages"
|
|
468
|
+
|
|
469
|
+
return dt_center
|
|
470
|
+
|
|
471
|
+
|
|
359
472
|
def _mk_test(x, alpha=0.05):
|
|
360
473
|
'''
|
|
361
474
|
Mann-Kendall test of significance for trend (used in classical
|
|
@@ -54,8 +54,8 @@ def degradation_summary_plots(yoy_rd, yoy_ci, yoy_info, normalized_yield,
|
|
|
54
54
|
Include extra information in the returned figure:
|
|
55
55
|
|
|
56
56
|
* Color code points by the number of times they get used in calculating
|
|
57
|
-
Rd slopes. Default color:
|
|
58
|
-
|
|
57
|
+
Rd slopes. Default color: even times (as a start and endpoint). Green:
|
|
58
|
+
odd times. Red: 0 times.
|
|
59
59
|
* The number of year-on-year slopes contributing to the histogram.
|
|
60
60
|
|
|
61
61
|
Note
|
|
@@ -109,7 +109,11 @@ def degradation_summary_plots(yoy_rd, yoy_ci, yoy_info, normalized_yield,
|
|
|
109
109
|
|
|
110
110
|
renormalized_yield = normalized_yield / yoy_info['renormalizing_factor']
|
|
111
111
|
if detailed:
|
|
112
|
-
|
|
112
|
+
# Color by usage parity: 0 -> red, odd -> green, even/non-zero or NaN -> plot_color
|
|
113
|
+
usage = yoy_info['usage_of_points']
|
|
114
|
+
colors = pd.Series(plot_color, index=usage.index)
|
|
115
|
+
colors[usage == 0] = 'red'
|
|
116
|
+
colors[usage % 2 == 1] = 'green'
|
|
113
117
|
else:
|
|
114
118
|
colors = plot_color
|
|
115
119
|
ax1.scatter(
|
|
@@ -432,7 +436,8 @@ def availability_summary_plots(power_system, power_subsystem, loss_total,
|
|
|
432
436
|
|
|
433
437
|
|
|
434
438
|
def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True,
|
|
435
|
-
fig=None, plot_color=None, ci_color=None,
|
|
439
|
+
fig=None, plot_color=None, ci_color=None,
|
|
440
|
+
center=None, min_periods_divisor=None, **kwargs):
|
|
436
441
|
'''
|
|
437
442
|
Plot resampled time series of degradation trend with time
|
|
438
443
|
|
|
@@ -441,10 +446,14 @@ def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True,
|
|
|
441
446
|
yoy_info : dict
|
|
442
447
|
a dictionary with keys:
|
|
443
448
|
|
|
444
|
-
* YoY_values - pandas series of
|
|
449
|
+
* YoY_values - pandas series of year on year slopes with integer index.
|
|
450
|
+
* YoY_times - pandas DataFrame containing a ``dt_left``, ``dt_center``
|
|
451
|
+
and ``dt_right`` timestamp columns, indexed by the same integer window
|
|
452
|
+
id as ``YoY_values``.
|
|
445
453
|
rolling_days: int, default 365
|
|
446
|
-
Number of days for rolling window.
|
|
447
|
-
|
|
454
|
+
Number of days for rolling window. The window must contain at least
|
|
455
|
+
``rolling_days // min_periods_divisor`` datapoints to be included in
|
|
456
|
+
the rolling plot.
|
|
448
457
|
include_ci : bool, default True
|
|
449
458
|
calculate and plot 2-sigma confidence intervals along with rolling median
|
|
450
459
|
fig : matplotlib, optional
|
|
@@ -453,6 +462,21 @@ def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True,
|
|
|
453
462
|
color of the timeseries trendline
|
|
454
463
|
ci_color : str, optional
|
|
455
464
|
color of the confidence interval 'fuzz'
|
|
465
|
+
center : bool, default False
|
|
466
|
+
If ``True``, the rolling window is centered and ``results_values`` is
|
|
467
|
+
reindexed using ``yoy_info['YoY_times']['dt_center']`` before any calculations are
|
|
468
|
+
performed. The recommended value is ``True``; the default of ``False``
|
|
469
|
+
is retained only for backward compatibility. A warning is raised when
|
|
470
|
+
this argument is not explicitly supplied.
|
|
471
|
+
min_periods_divisor : int, optional
|
|
472
|
+
Divisor applied to ``rolling_days`` to set the minimum number of
|
|
473
|
+
observations required in a window. Smaller values (e.g. 2) require
|
|
474
|
+
the window to be more populated; larger values (e.g. 4) make the
|
|
475
|
+
plot more resilient to small data outages without losing fidelity.
|
|
476
|
+
Defaults to 2 in this release to match the behavior in rdtools
|
|
477
|
+
prior to the multi-YoY changes. A ``FutureWarning`` is emitted when
|
|
478
|
+
the default is used; the default will change to 4 in a future major
|
|
479
|
+
release. Pass an explicit value to silence the warning.
|
|
456
480
|
kwargs :
|
|
457
481
|
Extra parameters passed to matplotlib.pyplot.axis.plot()
|
|
458
482
|
|
|
@@ -466,6 +490,27 @@ def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True,
|
|
|
466
490
|
matplotlib.figure.Figure
|
|
467
491
|
'''
|
|
468
492
|
|
|
493
|
+
if center is None:
|
|
494
|
+
warnings.warn(
|
|
495
|
+
"The default value of 'center' will remain False for backward "
|
|
496
|
+
"compatibility, but center=True is recommended. Pass "
|
|
497
|
+
"center=True to silence this warning.",
|
|
498
|
+
UserWarning,
|
|
499
|
+
stacklevel=2,
|
|
500
|
+
)
|
|
501
|
+
center = False
|
|
502
|
+
|
|
503
|
+
if min_periods_divisor is None:
|
|
504
|
+
warnings.warn(
|
|
505
|
+
"The default `min_periods_divisor=2` will change to 4 in a future "
|
|
506
|
+
"major release of rdtools, which makes the rolling plot more "
|
|
507
|
+
"resilient to small data outages. Pass `min_periods_divisor` "
|
|
508
|
+
"explicitly to silence this warning.",
|
|
509
|
+
FutureWarning,
|
|
510
|
+
stacklevel=2,
|
|
511
|
+
)
|
|
512
|
+
min_periods_divisor = 2
|
|
513
|
+
|
|
469
514
|
def _bootstrap(x, percentile, reps):
|
|
470
515
|
# stolen from degradation_year_on_year
|
|
471
516
|
n1 = len(x)
|
|
@@ -474,29 +519,52 @@ def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True,
|
|
|
474
519
|
return np.percentile(mb1, percentile)
|
|
475
520
|
|
|
476
521
|
try:
|
|
477
|
-
results_values = yoy_info['YoY_values']
|
|
522
|
+
results_values = yoy_info['YoY_values'].copy()
|
|
478
523
|
|
|
479
524
|
except KeyError:
|
|
480
525
|
raise KeyError("yoy_info input dictionary does not contain key `YoY_values`.")
|
|
481
526
|
|
|
527
|
+
# filter to only 2 years + 1 day length slopes to avoid over-smoothing in the multi-yoy case
|
|
528
|
+
# (applied before index reassignment while integer index still aligns with YoY_times)
|
|
529
|
+
yoy_durations = yoy_info['YoY_times']['dt_right'] - yoy_info['YoY_times']['dt_left']
|
|
530
|
+
results_values = results_values[
|
|
531
|
+
results_values.index.map(yoy_durations) <= pd.Timedelta(days=365 * 2 + 1)
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
if center:
|
|
535
|
+
try:
|
|
536
|
+
results_values.index = results_values.index.map(yoy_info['YoY_times']['dt_center'])
|
|
537
|
+
except KeyError:
|
|
538
|
+
raise KeyError("yoy_info input dict doesn't contain key `YoY_times['dt_center']`, "
|
|
539
|
+
"which is required when center=True.")
|
|
540
|
+
else:
|
|
541
|
+
results_values.index = results_values.index.map(yoy_info['YoY_times']['dt_right'])
|
|
542
|
+
|
|
543
|
+
results_values = results_values.sort_index()
|
|
544
|
+
|
|
482
545
|
if plot_color is None:
|
|
483
546
|
plot_color = 'tab:orange'
|
|
484
547
|
if ci_color is None:
|
|
485
548
|
ci_color = 'C0'
|
|
486
549
|
|
|
487
|
-
roller = results_values.rolling(f'{rolling_days}
|
|
488
|
-
|
|
489
|
-
|
|
550
|
+
roller = results_values.rolling(f'{rolling_days}D',
|
|
551
|
+
min_periods=rolling_days // min_periods_divisor,
|
|
552
|
+
center=center)
|
|
553
|
+
|
|
490
554
|
if include_ci:
|
|
491
555
|
ci_lower = roller.apply(_bootstrap, kwargs={'percentile': 2.5, 'reps': 100}, raw=True)
|
|
492
556
|
ci_upper = roller.apply(_bootstrap, kwargs={'percentile': 97.5, 'reps': 100}, raw=True)
|
|
557
|
+
ci_lower = ci_lower[~ci_lower.index.duplicated(keep='last')]
|
|
558
|
+
ci_upper = ci_upper[~ci_upper.index.duplicated(keep='last')]
|
|
559
|
+
rolling_median = roller.median()
|
|
560
|
+
rolling_median = rolling_median[~rolling_median.index.duplicated(keep='last')]
|
|
493
561
|
if fig is None:
|
|
494
562
|
fig, ax = plt.subplots()
|
|
495
563
|
else:
|
|
496
564
|
ax = fig.axes[0]
|
|
497
565
|
if include_ci:
|
|
498
566
|
ax.fill_between(ci_lower.index, ci_lower, ci_upper, color=ci_color)
|
|
499
|
-
ax.plot(
|
|
567
|
+
ax.plot(rolling_median, color=plot_color, **kwargs)
|
|
500
568
|
ax.axhline(results_values.median(), c='k', ls='--')
|
|
501
569
|
plt.ylabel('Degradation trend (%/yr)')
|
|
502
570
|
fig.autofmt_xdate()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|