Quickstart

Showcase for how this package can be used.

Example data

Let’s start by importing the package and getting some mock example timeseries in hourly resolution for a certain customer portfolio.

[1]:
import portfolyo as pf
import pandas as pd

index = pd.date_range("2024", freq="h", periods=8784, tz="Europe/Berlin")

# Creating market prices (here: forward price curve) timeseries.
ts_prices = pf.dev.p_marketprices(index, avg=200)

# Creating portfolio offtake timeseries.
ts_offtake = -1 * pf.dev.w_offtake(index, avg=50)
# Creating portfolio sourced volume and prices timeseries (of quarter-products).
ts_sourced_power, ts_sourced_price = pf.dev.wp_sourced(ts_offtake, "QS", 0.3, p_avg=120)

Portfolio lines

By turning the timeseries into 3 “portfolio lines”…

[2]:
hpfc = pf.PfLine({"p": ts_prices})  # price-only
offtake = pf.PfLine({"w": ts_offtake})  # volume-only
sourced = pf.PfLine({"w": ts_sourced_power, "p": ts_sourced_price})  # price-and-volume

…it becomes easier to do common operations like plotting:

[3]:
sourced.plot();
../_images/tutorial_quickstart_5_0.png

…aggregating:

[4]:
sourced.asfreq("MS")
[4]:
PfLine object with price and volume information.
. Start: 2024-01-01 00:00:00+01:00 (incl)    . Timezone    : Europe/Berlin
. End  : 2025-01-01 00:00:00+01:00 (excl)    . Start-of-day: 00:00:00
. Freq : <MonthBegin> (12 datapoints)
                                     w           q           p             r
                                    MW         MWh     Eur/MWh           Eur

2024-01-01 00:00:00 +0100         14.9      11 053      131.46     1 453 063
2024-02-01 00:00:00 +0100         14.9      10 398      131.69     1 369 327
2024-03-01 00:00:00 +0100         15.2      11 259      132.27     1 489 298
2024-04-01 00:00:00 +0200         11.5       8 306      115.69       960 908
2024-05-01 00:00:00 +0200         11.6       8 628      115.52       996 763
2024-06-01 00:00:00 +0200         11.1       7 967      117.02       932 229
2024-07-01 00:00:00 +0200         10.4       7 756      111.58       865 442
2024-08-01 00:00:00 +0200         10.4       7 765      110.89       861 142
2024-09-01 00:00:00 +0200         10.4       7 518      110.69       832 114
2024-10-01 00:00:00 +0200         17.0      12 685      124.97     1 585 180
2024-11-01 00:00:00 +0100         16.7      12 015      124.08     1 490 818
2024-12-01 00:00:00 +0100         16.8      12 475      124.29     1 550 568

…or decomposing into peak- and offpeak values:

[5]:
sourced.po("QS").pint.dequantify()  # .pint.dequantify() to show units in column header
c:\Users\ruud.wijtvliet\Anaconda3\envs\pf38\lib\site-packages\pint_pandas\pint_array.py:194: RuntimeWarning: pint-pandas does not support magnitudes of <class 'int'>. Converting magnitudes to float.
  warnings.warn(
[5]:
peak offpeak
duration w q p r duration w q p r
unit h MW MW·h Eur/MWh Eur h MW MW·h Eur/MWh Eur
2024-01-01 00:00:00+01:00 780.0 8.969180 6995.960351 104.407521 7.304309e+05 1403.0 18.327806 25713.911223 139.273118 3.581257e+06
2024-04-01 00:00:00+02:00 780.0 20.491010 15982.987566 102.065215 1.631307e+06 1404.0 6.351754 8917.862478 141.131612 1.258592e+06
2024-07-01 00:00:00+02:00 792.0 9.947104 7878.105974 139.604102 1.099816e+06 1416.0 10.707181 15161.368636 96.223647 1.458882e+06
2024-10-01 00:00:00+02:00 792.0 27.475675 21760.734601 141.442611 3.077895e+06 1417.0 10.877807 15413.852057 100.472664 1.548671e+06

Portfolio state

We can do even more useful analyses by combining the portfolio lines for offtake, sourced, and market prices into a single “portfolio state”:

[6]:
pfs = pf.PfState(offtake, hpfc, sourced)

pfs
[6]:
PfState object.
. Start: 2024-01-01 00:00:00+01:00 (incl)    . Timezone    : Europe/Berlin
. End  : 2025-01-01 00:00:00+01:00 (excl)    . Start-of-day: 00:00:00
. Freq : <Hour> (8784 datapoints)
                                                w           q           p             r
                                               MW         MWh     Eur/MWh           Eur
──────── offtake
           2024-01-01 00:00:00 +0100        -56.6         -57
           2024-01-01 01:00:00 +0100        -52.8         -53
           ..                                  ..          ..          ..            ..
           2024-12-31 22:00:00 +0100        -67.7         -68
           2024-12-31 23:00:00 +0100        -62.7         -63
─●────── pnl_cost
 │         2024-01-01 00:00:00 +0100         56.6          57      201.02        11 372
 │         2024-01-01 01:00:00 +0100         52.8          53      190.18        10 033
 │         ..                                  ..          ..          ..            ..
 │         2024-12-31 22:00:00 +0100         67.7          68      194.22        13 144
 │         2024-12-31 23:00:00 +0100         62.7          63      203.81        12 773
 ├────── sourced
 │         2024-01-01 00:00:00 +0100         18.3          18      139.27         2 553
 │         2024-01-01 01:00:00 +0100         18.3          18      139.27         2 553
 │         ..                                  ..          ..          ..            ..
 │         2024-12-31 22:00:00 +0100         10.9          11      100.47         1 093
 │         2024-12-31 23:00:00 +0100         10.9          11      100.47         1 093
 └────── unsourced
           2024-01-01 00:00:00 +0100         38.2          38      230.62         8 820
           2024-01-01 01:00:00 +0100         34.4          34      217.28         7 481
           ..                                  ..          ..          ..            ..
           2024-12-31 22:00:00 +0100         56.8          57      212.18        12 051
           2024-12-31 23:00:00 +0100         51.8          52      225.51        11 680

This object can also be resampled to other frequencies:

[7]:
pfs_months = pfs.asfreq("MS")

We can see how much of the offtake volume is still unsourced, and how much we expect to pay for it, as a portfolio line. We can obtain it in the original frequency with pfs.unsourced, or in monthly frequency with pfs_monthly.unsourced. We can of course also chain the methods:

[8]:
pfs.asfreq("MS").unsourced  # or: pfs.unsourced.asfreq('MS')
[8]:
PfLine object with price and volume information.
. Start: 2024-01-01 00:00:00+01:00 (incl)    . Timezone    : Europe/Berlin
. End  : 2025-01-01 00:00:00+01:00 (excl)    . Start-of-day: 00:00:00
. Freq : <MonthBegin> (12 datapoints)
                                     w           q           p             r
                                    MW         MWh     Eur/MWh           Eur

2024-01-01 00:00:00 +0100         49.7      36 990      255.70     9 458 395
2024-02-01 00:00:00 +0100         48.2      33 567      240.85     8 084 500
2024-03-01 00:00:00 +0100         44.2      32 848      213.46     7 011 828
2024-04-01 00:00:00 +0200         43.3      31 147      174.37     5 431 217
2024-05-01 00:00:00 +0200         38.3      28 484      145.33     4 139 700
2024-06-01 00:00:00 +0200         34.6      24 882      123.65     3 076 521
2024-07-01 00:00:00 +0200         34.4      25 583      136.56     3 493 569
2024-08-01 00:00:00 +0200         35.5      26 390      145.96     3 851 887
2024-09-01 00:00:00 +0200         39.0      28 057      170.99     4 797 520
2024-10-01 00:00:00 +0200         37.7      28 062      193.33     5 425 100
2024-11-01 00:00:00 +0100         42.6      30 699      218.76     6 715 852
2024-12-01 00:00:00 +0100         46.3      34 422      238.51     8 209 875

Plotting is also possible:

[9]:
pfs.plot();
../_images/tutorial_quickstart_17_0.png

We can see how much it would cost to perfectly hedge the entire offtake volume, based on what has already been sourced (and its price) and what is still unsourced (valued at market prices). This is available at pfl.pnl_cost. Let’s look at this, graphically, in month values:

[10]:
pfs.pnl_cost.asfreq("MS").plot();
../_images/tutorial_quickstart_19_0.png

On average, only about 30% of the portfolio has currently been hedged. This can be seen from the pfs.plot() output, or we can calculate the fractions explicitly:

[11]:
pfs.asfreq("MS").sourcedfraction  # not pfs.sourcedfraction.asfreq("MS") !
# (.sourcedfraction is a pandas Series, which does not average when applying .asfreq("MS"))
[11]:
2024-01-01 00:00:00+01:00    0.23006370221556163
2024-02-01 00:00:00+01:00    0.23650210243151373
2024-03-01 00:00:00+01:00     0.2552667409134675
2024-04-01 00:00:00+02:00     0.2105305001508305
2024-05-01 00:00:00+02:00     0.2324871900458479
2024-06-01 00:00:00+02:00     0.2425302180631649
2024-07-01 00:00:00+02:00    0.23264967847113516
2024-08-01 00:00:00+02:00    0.22735797840772143
2024-09-01 00:00:00+02:00     0.2113212510546372
2024-10-01 00:00:00+02:00    0.31131448354888985
2024-11-01 00:00:00+01:00    0.28128161731884777
2024-12-01 00:00:00+01:00    0.26600630185450264
Freq: MS, Name: fraction, dtype: pint[]

This means that an increase in the market prices will have a relatively large impact, especially in the months with a low sourced fraction. Let’s verify that.

First, we create a new price curve, and then create a new portfolio state with it. Then, we again look at the .pnl_cost property, to see the new procurement prices:

[12]:
hpfc2 = hpfc + pf.Q_(100.0, "Eur/MWh")
pfs2 = pfs.set_unsourcedprice(hpfc2)
pfs2.pnl_cost.asfreq("MS").plot();
../_images/tutorial_quickstart_23_0.png

We could even explicitly calculate the monthly price increases:

[13]:
pfs2.asfreq("MS").pnl_cost.p - pfs.asfreq("MS").pnl_cost.p
[13]:
2024-01-01 00:00:00+01:00    76.99362977844379
2024-02-01 00:00:00+01:00     76.3497897568486
2024-03-01 00:00:00+01:00    74.47332590865318
2024-04-01 00:00:00+02:00    78.94694998491693
2024-05-01 00:00:00+02:00    76.75128099541516
2024-06-01 00:00:00+02:00    75.74697819368352
2024-07-01 00:00:00+02:00    76.73503215288648
2024-08-01 00:00:00+02:00    77.26420215922792
2024-09-01 00:00:00+02:00    78.86787489453633
2024-10-01 00:00:00+02:00    68.86855164511101
2024-11-01 00:00:00+01:00     71.8718382681152
2024-12-01 00:00:00+01:00    73.39936981454974
Freq: MS, Name: p, dtype: pint[Eur/MWh]

Indeed, the 100 Eur/MWh across-the-board market price increase has impacted the months exactly in proportion to the fraction that is still unsourced. So, a month that is 60% hedged sees a 40 Eur/MWh impact in its procurement price.

That’s it for this quick introduction. Have a look at the more in-depth tutorial or the more systematic documentation of the PfLine or PfState classes.