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.
Files changed (29) hide show
  1. {rdtools-3.1.1/rdtools.egg-info → rdtools-3.2.0}/PKG-INFO +1 -1
  2. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/_version.py +3 -3
  3. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/analysis_chains.py +14 -5
  4. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/degradation.py +136 -23
  5. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/plotting.py +80 -12
  6. {rdtools-3.1.1 → rdtools-3.2.0/rdtools.egg-info}/PKG-INFO +1 -1
  7. {rdtools-3.1.1 → rdtools-3.2.0}/LICENSE +0 -0
  8. {rdtools-3.1.1 → rdtools-3.2.0}/MANIFEST.in +0 -0
  9. {rdtools-3.1.1 → rdtools-3.2.0}/README.md +0 -0
  10. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/__init__.py +0 -0
  11. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/_deprecation.py +0 -0
  12. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/aggregation.py +0 -0
  13. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/availability.py +0 -0
  14. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/bootstrap.py +0 -0
  15. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/clearsky_temperature.py +0 -0
  16. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/data/temperature.hdf5 +0 -0
  17. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/filtering.py +0 -0
  18. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/models/xgboost_clipping_model.json +0 -0
  19. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/normalization.py +0 -0
  20. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/soiling.py +0 -0
  21. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools/utilities.py +0 -0
  22. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/SOURCES.txt +0 -0
  23. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/dependency_links.txt +0 -0
  24. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/not-zip-safe +0 -0
  25. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/requires.txt +0 -0
  26. {rdtools-3.1.1 → rdtools-3.2.0}/rdtools.egg-info/top_level.txt +0 -0
  27. {rdtools-3.1.1 → rdtools-3.2.0}/setup.cfg +0 -0
  28. {rdtools-3.1.1 → rdtools-3.2.0}/setup.py +0 -0
  29. {rdtools-3.1.1 → rdtools-3.2.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdtools
3
- Version: 3.1.1
3
+ Version: 3.2.0
4
4
  Summary: Functions for reproducible timeseries analysis of photovoltaic systems.
5
5
  Home-page: https://github.com/NREL/rdtools
6
6
  Author: Rdtools Python Developers
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-03-19T12:53:21-0400",
11
+ "date": "2026-07-01T15:51:37-0400",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "0f8e3e739bf0ba9688b8d35fc799938b5935cfbf",
15
- "version": "3.1.1"
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. Note that the window must contain
1261
- at least 50% of datapoints to be included in rolling plot.
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 right-labeled year on year slopes
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
- energy_normalized['dt_shifted'] = energy_normalized.dt + pd.DateOffset(years=1)
273
-
274
- # Merge with what happened one year ago, use tolerance of 8 days to allow
275
- # for weekly aggregated data
276
- df = pd.merge_asof(energy_normalized[['dt', 'energy']],
277
- energy_normalized.sort_values('dt_shifted'),
278
- left_on='dt', right_on='dt_shifted',
279
- suffixes=['', '_right'],
280
- tolerance=pd.Timedelta('8D')
281
- )
282
-
283
- df['time_diff_years'] = (df.dt - df.dt_right) / pd.Timedelta('365D')
284
- df['yoy'] = 100.0 * (df.energy - df.energy_right) / (df.time_diff_years)
285
- df.index = df.dt
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': df['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': df['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: 2 times (as a start and endpoint). Green:
58
- 1 time. Red: 0 times.
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
- colors = yoy_info['usage_of_points'].map({0: 'red', 1: 'green', 2: plot_color})
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, **kwargs):
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 right-labeled year on year slopes
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. Note that the window must contain
447
- at least 50% of datapoints to be included in rolling plot.
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}d', min_periods=rolling_days//2)
488
- # unfortunately it seems that you can't return multiple values in the rolling.apply() kernel.
489
- # TODO: figure out some workaround to return both percentiles in a single pass
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(roller.median(), color=plot_color, **kwargs)
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdtools
3
- Version: 3.1.1
3
+ Version: 3.2.0
4
4
  Summary: Functions for reproducible timeseries analysis of photovoltaic systems.
5
5
  Home-page: https://github.com/NREL/rdtools
6
6
  Author: Rdtools Python Developers
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