diff --git a/demo_energiedelen.ipynb b/demo_energiedelen.ipynb
index f7ecd1b..56fda2c 100644
--- a/demo_energiedelen.ipynb
+++ b/demo_energiedelen.ipynb
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from openenergyid import TimeSeries\n",
+ "from openenergyid import TimeDataFrame\n",
"from openenergyid.energysharing import EnergySharingInput, EnergySharingOutput, calculate, CalculationMethod"
]
},
@@ -386,9 +386,9 @@
"# This helps with validation and serialization\n",
"\n",
"input = EnergySharingInput.model_construct(\n",
- " gross_injection=TimeSeries.from_pandas(gross_injection),\n",
- " gross_offtake=TimeSeries.from_pandas(gross_offtake),\n",
- " key=TimeSeries.from_pandas(key),\n",
+ " gross_injection=TimeDataFrame.from_pandas(gross_injection),\n",
+ " gross_offtake=TimeDataFrame.from_pandas(gross_offtake),\n",
+ " key=TimeDataFrame.from_pandas(key),\n",
")\n",
"\n",
"df = input.data_frame()"
@@ -449,18 +449,18 @@
"
\n",
" \n",
" 2024-01-01 12:00:00+01:00 | \n",
- " 70.0 | \n",
- " 10.0 | \n",
- " 20.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 30.0 | \n",
- " 22.0 | \n",
- " 30.0 | \n",
- " 18.0 | \n",
- " 20.0 | \n",
+ " 70 | \n",
+ " 10 | \n",
+ " 20 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 30 | \n",
+ " 22 | \n",
+ " 30 | \n",
+ " 18 | \n",
+ " 20 | \n",
" 0.0 | \n",
" 0.1 | \n",
" 0.225 | \n",
@@ -470,18 +470,18 @@
"
\n",
" \n",
" 2024-01-01 12:15:00+01:00 | \n",
- " 20.0 | \n",
- " 10.0 | \n",
- " 30.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 20.0 | \n",
- " 20.0 | \n",
- " 25.0 | \n",
- " 17.0 | \n",
- " 22.0 | \n",
+ " 20 | \n",
+ " 10 | \n",
+ " 30 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 20 | \n",
+ " 20 | \n",
+ " 25 | \n",
+ " 17 | \n",
+ " 22 | \n",
" 0.0 | \n",
" 0.1 | \n",
" 0.225 | \n",
@@ -491,18 +491,18 @@
"
\n",
" \n",
" 2024-01-01 12:30:00+01:00 | \n",
- " 100.0 | \n",
- " 10.0 | \n",
- " 40.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 10.0 | \n",
- " 30.0 | \n",
- " 35.0 | \n",
- " 19.0 | \n",
- " 24.0 | \n",
+ " 100 | \n",
+ " 10 | \n",
+ " 40 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 10 | \n",
+ " 30 | \n",
+ " 35 | \n",
+ " 19 | \n",
+ " 24 | \n",
" 0.0 | \n",
" 0.1 | \n",
" 0.225 | \n",
@@ -512,18 +512,18 @@
"
\n",
" \n",
" 2024-01-01 12:45:00+01:00 | \n",
- " 0.0 | \n",
- " 10.0 | \n",
- " 50.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 0.0 | \n",
- " 40.0 | \n",
- " 10.0 | \n",
- " 25.0 | \n",
- " 26.0 | \n",
+ " 0 | \n",
+ " 10 | \n",
+ " 50 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 40 | \n",
+ " 10 | \n",
+ " 25 | \n",
+ " 26 | \n",
" 0.0 | \n",
" 0.1 | \n",
" 0.225 | \n",
@@ -536,26 +536,26 @@
""
],
"text/plain": [
- " Gross Injection \\\n",
- " P1 P2 P3 P4 P5 P6 \n",
- "2024-01-01 12:00:00+01:00 70.0 10.0 20.0 0.0 0.0 0.0 \n",
- "2024-01-01 12:15:00+01:00 20.0 10.0 30.0 0.0 0.0 0.0 \n",
- "2024-01-01 12:30:00+01:00 100.0 10.0 40.0 0.0 0.0 0.0 \n",
- "2024-01-01 12:45:00+01:00 0.0 10.0 50.0 0.0 0.0 0.0 \n",
+ " Gross Injection Gross Offtake \\\n",
+ " P1 P2 P3 P4 P5 P6 P1 P2 \n",
+ "2024-01-01 12:00:00+01:00 70 10 20 0 0 0 0 30 \n",
+ "2024-01-01 12:15:00+01:00 20 10 30 0 0 0 0 20 \n",
+ "2024-01-01 12:30:00+01:00 100 10 40 0 0 0 0 10 \n",
+ "2024-01-01 12:45:00+01:00 0 10 50 0 0 0 0 0 \n",
"\n",
- " Gross Offtake Key \\\n",
- " P1 P2 P3 P4 P5 P6 P1 \n",
- "2024-01-01 12:00:00+01:00 0.0 30.0 22.0 30.0 18.0 20.0 0.0 \n",
- "2024-01-01 12:15:00+01:00 0.0 20.0 20.0 25.0 17.0 22.0 0.0 \n",
- "2024-01-01 12:30:00+01:00 0.0 10.0 30.0 35.0 19.0 24.0 0.0 \n",
- "2024-01-01 12:45:00+01:00 0.0 0.0 40.0 10.0 25.0 26.0 0.0 \n",
+ " Key \\\n",
+ " P3 P4 P5 P6 P1 P2 P3 P4 P5 \n",
+ "2024-01-01 12:00:00+01:00 22 30 18 20 0.0 0.1 0.225 0.225 0.225 \n",
+ "2024-01-01 12:15:00+01:00 20 25 17 22 0.0 0.1 0.225 0.225 0.225 \n",
+ "2024-01-01 12:30:00+01:00 30 35 19 24 0.0 0.1 0.225 0.225 0.225 \n",
+ "2024-01-01 12:45:00+01:00 40 10 25 26 0.0 0.1 0.225 0.225 0.225 \n",
"\n",
- " \n",
- " P2 P3 P4 P5 P6 \n",
- "2024-01-01 12:00:00+01:00 0.1 0.225 0.225 0.225 0.225 \n",
- "2024-01-01 12:15:00+01:00 0.1 0.225 0.225 0.225 0.225 \n",
- "2024-01-01 12:30:00+01:00 0.1 0.225 0.225 0.225 0.225 \n",
- "2024-01-01 12:45:00+01:00 0.1 0.225 0.225 0.225 0.225 "
+ " \n",
+ " P6 \n",
+ "2024-01-01 12:00:00+01:00 0.225 \n",
+ "2024-01-01 12:15:00+01:00 0.225 \n",
+ "2024-01-01 12:30:00+01:00 0.225 \n",
+ "2024-01-01 12:45:00+01:00 0.225 "
]
},
"execution_count": 9,
@@ -1155,6 +1155,12 @@
"text": [
"{\n",
" \"netInjection\": {\n",
+ " \"index\": [\n",
+ " \"2024-01-01T12:00:00+01:00\",\n",
+ " \"2024-01-01T12:15:00+01:00\",\n",
+ " \"2024-01-01T12:30:00+01:00\",\n",
+ " \"2024-01-01T12:45:00+01:00\"\n",
+ " ],\n",
" \"columns\": [\n",
" \"P1\",\n",
" \"P2\",\n",
@@ -1163,12 +1169,6 @@
" \"P5\",\n",
" \"P6\"\n",
" ],\n",
- " \"index\": [\n",
- " \"2024-01-01T12:00:00+01:00\",\n",
- " \"2024-01-01T12:15:00+01:00\",\n",
- " \"2024-01-01T12:30:00+01:00\",\n",
- " \"2024-01-01T12:45:00+01:00\"\n",
- " ],\n",
" \"data\": [\n",
" [\n",
" 0.0,\n",
@@ -1205,6 +1205,12 @@
" ]\n",
" },\n",
" \"netOfftake\": {\n",
+ " \"index\": [\n",
+ " \"2024-01-01T12:00:00+01:00\",\n",
+ " \"2024-01-01T12:15:00+01:00\",\n",
+ " \"2024-01-01T12:30:00+01:00\",\n",
+ " \"2024-01-01T12:45:00+01:00\"\n",
+ " ],\n",
" \"columns\": [\n",
" \"P1\",\n",
" \"P2\",\n",
@@ -1213,12 +1219,6 @@
" \"P5\",\n",
" \"P6\"\n",
" ],\n",
- " \"index\": [\n",
- " \"2024-01-01T12:00:00+01:00\",\n",
- " \"2024-01-01T12:15:00+01:00\",\n",
- " \"2024-01-01T12:30:00+01:00\",\n",
- " \"2024-01-01T12:45:00+01:00\"\n",
- " ],\n",
" \"data\": [\n",
" [\n",
" 0.0,\n",
@@ -1255,6 +1255,12 @@
" ]\n",
" },\n",
" \"sharedEnergy\": {\n",
+ " \"index\": [\n",
+ " \"2024-01-01T12:00:00+01:00\",\n",
+ " \"2024-01-01T12:15:00+01:00\",\n",
+ " \"2024-01-01T12:30:00+01:00\",\n",
+ " \"2024-01-01T12:45:00+01:00\"\n",
+ " ],\n",
" \"columns\": [\n",
" \"P1\",\n",
" \"P2\",\n",
@@ -1263,12 +1269,6 @@
" \"P5\",\n",
" \"P6\"\n",
" ],\n",
- " \"index\": [\n",
- " \"2024-01-01T12:00:00+01:00\",\n",
- " \"2024-01-01T12:15:00+01:00\",\n",
- " \"2024-01-01T12:30:00+01:00\",\n",
- " \"2024-01-01T12:45:00+01:00\"\n",
- " ],\n",
" \"data\": [\n",
" [\n",
" 0.0,\n",
diff --git a/demo_energyid_download.ipynb b/demo_energyid_download.ipynb
index 69f0529..61bb451 100644
--- a/demo_energyid_download.ipynb
+++ b/demo_energyid_download.ipynb
@@ -24,7 +24,7 @@
"source": [
"import json\n",
"import energyid\n",
- "from openenergyid import TimeSeries, const"
+ "from openenergyid import TimeDataFrame, const"
]
},
{
@@ -98,9 +98,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Rename to standard metric names used in the Open Energy Library\n",
- "\n",
- "energy_balance.columns = [const.ELECTRICITY_DELIVERED, const.ELECTRICITY_PRODUCED, const.ELECTRICITY_EXPORTED]"
+ "energy_balance"
]
},
{
@@ -109,7 +107,9 @@
"metadata": {},
"outputs": [],
"source": [
- "energy_balance.index.name = None"
+ "# Rename to standard metric names used in the Open Energy Library\n",
+ "\n",
+ "energy_balance.columns = [const.ELECTRICITY_DELIVERED, const.ELECTRICITY_PRODUCED, const.ELECTRICITY_EXPORTED]"
]
},
{
@@ -118,7 +118,7 @@
"metadata": {},
"outputs": [],
"source": [
- "series = TimeSeries.from_pandas(energy_balance)"
+ "data = TimeDataFrame.from_pandas(energy_balance)"
]
},
{
@@ -127,7 +127,7 @@
"metadata": {},
"outputs": [],
"source": [
- "series.to_json(path='data/energy_balance_PT15M.json', indent=2)"
+ "data.to_json(path='data/energy_balance_PT15M.json', indent=2)"
]
}
],
diff --git a/openenergyid/__init__.py b/openenergyid/__init__.py
index 4fd4a2b..a49539a 100644
--- a/openenergyid/__init__.py
+++ b/openenergyid/__init__.py
@@ -3,6 +3,6 @@
__version__ = "0.1.10"
from .enums import Granularity
-from .models import TimeSeries
+from .models import TimeDataFrame, TimeSeries
-__all__ = ["Granularity", "TimeSeries"]
+__all__ = ["Granularity", "TimeDataFrame", "TimeSeries"]
diff --git a/openenergyid/energysharing/models.py b/openenergyid/energysharing/models.py
index 4835f14..084a2fa 100644
--- a/openenergyid/energysharing/models.py
+++ b/openenergyid/energysharing/models.py
@@ -6,7 +6,7 @@
from pydantic import BaseModel, Field
import pandas as pd
-from openenergyid import TimeSeries
+from openenergyid import TimeDataFrame
from .data_formatting import create_multi_index_input_frame
from .const import NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
@@ -22,9 +22,9 @@ class CalculationMethod(Enum):
class EnergySharingInput(BaseModel):
"""Input data for energy sharing."""
- gross_injection: Annotated[TimeSeries, Field(alias="grossInjection")]
- gross_offtake: Annotated[TimeSeries, Field(alias="grossOfftake")]
- key: Annotated[TimeSeries, Field(alias="key")]
+ gross_injection: Annotated[TimeDataFrame, Field(alias="grossInjection")]
+ gross_offtake: Annotated[TimeDataFrame, Field(alias="grossOfftake")]
+ key: Annotated[TimeDataFrame, Field(alias="key")]
timezone: str = Field(alias="timeZone", default="Europe/Brussels")
def data_frame(self) -> pd.DataFrame:
@@ -41,15 +41,15 @@ def data_frame(self) -> pd.DataFrame:
class EnergySharingOutput(BaseModel):
"""Output data for energy sharing."""
- net_injection: TimeSeries = Field(alias="netInjection")
- net_offtake: TimeSeries = Field(alias="netOfftake")
- shared_energy: TimeSeries = Field(alias="sharedEnergy")
+ net_injection: TimeDataFrame = Field(alias="netInjection")
+ net_offtake: TimeDataFrame = Field(alias="netOfftake")
+ shared_energy: TimeDataFrame = Field(alias="sharedEnergy")
@classmethod
def from_calculation_result(cls, result: pd.DataFrame) -> "EnergySharingOutput":
"""Create an output model from a calculation result."""
return cls.model_construct(
- net_injection=TimeSeries.from_pandas(result[NET_INJECTION]),
- net_offtake=TimeSeries.from_pandas(result[NET_OFFTAKE]),
- shared_energy=TimeSeries.from_pandas(result[SHARED_ENERGY]),
+ net_injection=TimeDataFrame.from_pandas(result[NET_INJECTION]),
+ net_offtake=TimeDataFrame.from_pandas(result[NET_OFFTAKE]),
+ shared_energy=TimeDataFrame.from_pandas(result[SHARED_ENERGY]),
)
diff --git a/openenergyid/models.py b/openenergyid/models.py
index 32c8828..b343324 100644
--- a/openenergyid/models.py
+++ b/openenergyid/models.py
@@ -1,67 +1,119 @@
"""Data models for the Open Energy ID."""
import datetime as dt
-from typing import Optional, overload
+from typing import Optional, overload, Union
+
+try:
+ from typing import Self
+except ImportError:
+ from typing_extensions import Self
import pandas as pd
from pydantic import BaseModel
-class TimeSeries(BaseModel):
- """Time series data."""
+class TimeSeriesBase(BaseModel):
+ """Pydantic base model for time series data."""
- columns: list[str]
index: list[dt.datetime]
- data: list[list[float]]
@classmethod
- def from_pandas(cls, data: pd.DataFrame) -> "TimeSeries":
- """Create a MultiVariableRegressionInputFrame from a pandas DataFrame."""
- return cls.model_validate(data.to_dict(orient="split"))
+ def from_pandas(cls, data: Union[pd.Series, pd.DataFrame]) -> Self:
+ """Create from a Pandas Object."""
+ raise NotImplementedError
- def to_pandas(self, timezone: str = "UTC") -> pd.DataFrame:
- """Convert the MultiVariableRegressionInputFrame to a pandas DataFrame."""
- frame = pd.DataFrame(self.data, columns=self.columns, index=self.index)
- frame.index = pd.to_datetime(frame.index, utc=True)
- return frame.tz_convert(timezone)
+ def to_pandas(self, timezone: str = "UTC") -> Union[pd.Series, pd.DataFrame]:
+ """Convert to a Pandas Object."""
+ raise NotImplementedError
@overload
def to_json(self, path: None = None, **kwargs) -> str:
- ...
+ """Dump to a JSON string."""
@overload
def to_json(self, path: str, **kwargs) -> None:
- ...
+ """Dump to a JSON file."""
def to_json(self, path: Optional[str] = None, **kwargs) -> Optional[str]:
- """Save the TimeSeries to a JSON file or return as string."""
+ """Dump to a JSON string or file."""
if path is None:
return self.model_dump_json(**kwargs)
- else:
- encoding = kwargs.pop("encoding", "UTF-8")
- with open(path, "w", encoding=encoding) as file:
- file.write(self.model_dump_json(**kwargs))
+ encoding = kwargs.pop("encoding", "UTF-8")
+ with open(path, "w", encoding=encoding) as file:
+ file.write(self.model_dump_json(**kwargs))
+ return None
@overload
@classmethod
- def from_json(cls, string: str, **kwargs) -> "TimeSeries":
- ...
+ def from_json(cls, string: str, **kwargs) -> Self:
+ """Load from a JSON string."""
@overload
@classmethod
- def from_json(cls, path: str, **kwargs) -> "TimeSeries":
- ...
+ def from_json(cls, path: str, **kwargs) -> Self:
+ """Load from a JSON file."""
@classmethod
- def from_json(
- cls, string: Optional[str] = None, path: Optional[str] = None, **kwargs
- ) -> "TimeSeries":
- """Load the TimeSeries from a JSON file or string."""
+ def from_json(cls, string: Optional[str] = None, path: Optional[str] = None, **kwargs) -> Self:
+ """Load from a JSON file or string."""
if string:
return cls.model_validate_json(string, **kwargs)
- elif path:
+ if path:
encoding = kwargs.pop("encoding", "UTF-8")
with open(path, "r", encoding=encoding) as file:
return cls.model_validate_json(file.read(), **kwargs)
- else:
- raise ValueError("Either string or path must be provided.")
+ raise ValueError("Either string or path must be provided.")
+
+
+class TimeSeries(TimeSeriesBase):
+ """Time series data with a single column."""
+
+ name: Union[str, None] = None
+ data: list[float]
+
+ @classmethod
+ def from_pandas(cls, data: pd.Series) -> Self:
+ """Create from a Pandas Series."""
+ return cls.model_construct(name=data.name, data=data.tolist(), index=data.index.tolist())
+
+ def to_pandas(self, timezone: str = "UTC") -> pd.Series:
+ """Convert to a Pandas Series."""
+ series = pd.Series(self.data, name=self.name, index=self.index)
+ series.index = pd.to_datetime(series.index, utc=True)
+ return series.tz_convert(timezone)
+
+
+class TimeDataFrame(TimeSeriesBase):
+ """Time series data with multiple columns."""
+
+ columns: list[str]
+ data: list[list[float]]
+
+ @classmethod
+ def from_pandas(cls, data: pd.DataFrame) -> Self:
+ """Create from a Pandas DataFrame."""
+ return cls.model_construct(
+ columns=data.columns.tolist(), data=data.values.tolist(), index=data.index.tolist()
+ )
+
+ def to_pandas(self, timezone: str = "UTC") -> pd.DataFrame:
+ """Convert to a Pandas DataFrame."""
+ frame = pd.DataFrame(self.data, columns=self.columns, index=self.index)
+ frame.index = pd.to_datetime(frame.index, utc=True)
+ return frame.tz_convert(timezone)
+
+ @classmethod
+ def from_timeseries(cls, data: list[TimeSeries]) -> Self:
+ """Create from a list of TimeSeries objects."""
+ return cls.model_construct(
+ columns=[series.name for series in data],
+ data=[series.data for series in data],
+ index=data[0].index,
+ )
+
+ def to_timeseries(self) -> list[TimeSeries]:
+ """Convert to a list of TimeSeries objects."""
+ return [
+ TimeSeries(name=column, data=column_data, index=self.index)
+ for column, column_data in zip(self.columns, self.data)
+ ]
diff --git a/openenergyid/mvlr/models.py b/openenergyid/mvlr/models.py
index 34ac7a5..bd067c7 100644
--- a/openenergyid/mvlr/models.py
+++ b/openenergyid/mvlr/models.py
@@ -6,7 +6,7 @@
import statsmodels.formula.api as fm
from openenergyid.enums import Granularity
-from openenergyid.models import TimeSeries
+from openenergyid.models import TimeDataFrame
from .mvlr import MultiVariableLinearRegression
@@ -66,7 +66,7 @@ class MultiVariableRegressionInput(BaseModel):
alias="independentVariables", min_length=1
)
dependent_variable: str = Field(alias="dependentVariable")
- frame: TimeSeries
+ frame: TimeDataFrame
granularities: list[Granularity]
allow_negative_predictions: bool = Field(alias="allowNegativePredictions", default=False)
validation_parameters: ValidationParameters = Field(
@@ -195,7 +195,7 @@ class MultiVariableRegressionResult(BaseModel):
prob_f_stat: float = Field(ge=0, le=1, alias="probFStat")
intercept: IndependentVariableResult
granularity: Granularity
- frame: TimeSeries
+ frame: TimeDataFrame
model_config = ConfigDict(populate_by_name=True)
@@ -225,5 +225,5 @@ def from_mvlr(cls, mvlr: MultiVariableLinearRegression) -> "MultiVariableRegress
prob_f_stat=mvlr.fit.f_pvalue,
intercept=IndependentVariableResult.from_fit(mvlr.fit, "Intercept"),
granularity=mvlr.granularity,
- frame=TimeSeries.from_pandas(frame),
+ frame=TimeDataFrame.from_pandas(frame),
)