Modulo per il calcolo delle statistiche e dei KPI della simulazione fotovoltaica.
This commit is contained in:
840
pvsim/statistics.py
Normal file
840
pvsim/statistics.py
Normal file
@@ -0,0 +1,840 @@
|
||||
"""
|
||||
pvsim/statistics.py
|
||||
|
||||
Modulo per il calcolo delle statistiche e dei KPI
|
||||
della simulazione fotovoltaica.
|
||||
|
||||
Livelli supportati:
|
||||
|
||||
Plant
|
||||
|
|
||||
+-- Inverter
|
||||
|
|
||||
+-- Combiner
|
||||
|
|
||||
+-- Panel
|
||||
|
||||
KPI principali:
|
||||
|
||||
- energia prodotta;
|
||||
- potenza media;
|
||||
- potenza massima;
|
||||
- potenza minima;
|
||||
- efficienza;
|
||||
- performance ratio;
|
||||
- availability;
|
||||
- fault count;
|
||||
- fault duration;
|
||||
- clipping;
|
||||
- yield specifico.
|
||||
|
||||
Il modulo riceve un SimulationResult e non modifica
|
||||
i dati della simulazione.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from .simulator import SimulationResult
|
||||
|
||||
from .utils import (
|
||||
normalize_dataframe,
|
||||
safe_divide,
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# KPI RESULT
|
||||
# ======================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class KPIResult:
|
||||
"""
|
||||
Contenitore dei KPI principali.
|
||||
"""
|
||||
|
||||
energy_Wh: float = 0.0
|
||||
|
||||
energy_kWh: float = 0.0
|
||||
|
||||
peak_power_W: float = 0.0
|
||||
|
||||
average_power_W: float = 0.0
|
||||
|
||||
minimum_power_W: float = 0.0
|
||||
|
||||
nominal_power_W: float = 0.0
|
||||
|
||||
efficiency: float = 0.0
|
||||
|
||||
performance_ratio: float = 0.0
|
||||
|
||||
availability: float = 0.0
|
||||
|
||||
fault_count: int = 0
|
||||
|
||||
clipping_loss_Wh: float = 0.0
|
||||
|
||||
specific_yield_kWh_kWp: float = 0.0
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
return {
|
||||
|
||||
"energy_Wh":
|
||||
self.energy_Wh,
|
||||
|
||||
"energy_kWh":
|
||||
self.energy_kWh,
|
||||
|
||||
"peak_power_W":
|
||||
self.peak_power_W,
|
||||
|
||||
"average_power_W":
|
||||
self.average_power_W,
|
||||
|
||||
"minimum_power_W":
|
||||
self.minimum_power_W,
|
||||
|
||||
"nominal_power_W":
|
||||
self.nominal_power_W,
|
||||
|
||||
"efficiency":
|
||||
self.efficiency,
|
||||
|
||||
"performance_ratio":
|
||||
self.performance_ratio,
|
||||
|
||||
"availability":
|
||||
self.availability,
|
||||
|
||||
"fault_count":
|
||||
self.fault_count,
|
||||
|
||||
"clipping_loss_Wh":
|
||||
self.clipping_loss_Wh,
|
||||
|
||||
"specific_yield_kWh_kWp":
|
||||
self.specific_yield_kWh_kWp,
|
||||
}
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# STATISTICS
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class SimulationStatistics:
|
||||
"""
|
||||
Calcola statistiche e KPI su SimulationResult.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
result: SimulationResult,
|
||||
) -> None:
|
||||
|
||||
self.result = result
|
||||
|
||||
# ==================================================================
|
||||
# DATAFRAME
|
||||
# ==================================================================
|
||||
|
||||
def plant_dataframe(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
|
||||
return normalize_dataframe(
|
||||
|
||||
self.result.plant_dataframe()
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def inverter_dataframe(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
|
||||
return normalize_dataframe(
|
||||
|
||||
self.result.inverter_dataframe()
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def combiner_dataframe(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
|
||||
return normalize_dataframe(
|
||||
|
||||
self.result.combiner_dataframe()
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def panel_dataframe(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
|
||||
return normalize_dataframe(
|
||||
|
||||
self.result.panel_dataframe()
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def faults_dataframe(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
|
||||
return normalize_dataframe(
|
||||
|
||||
self.result.faults_dataframe()
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# PLANT KPI
|
||||
# ==================================================================
|
||||
|
||||
def plant_kpi(
|
||||
self,
|
||||
) -> KPIResult:
|
||||
"""
|
||||
Calcola i KPI dell'intero impianto.
|
||||
"""
|
||||
|
||||
df = self.plant_dataframe()
|
||||
|
||||
if df.empty:
|
||||
|
||||
return KPIResult()
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Energia
|
||||
# --------------------------------------------------------------
|
||||
|
||||
if "energy_Wh" in df.columns:
|
||||
|
||||
energy_Wh = (
|
||||
|
||||
df[
|
||||
"energy_Wh"
|
||||
].sum()
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
energy_Wh = 0.0
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Potenza
|
||||
# --------------------------------------------------------------
|
||||
|
||||
power_column = (
|
||||
|
||||
"effective_ac_power_W"
|
||||
|
||||
if "effective_ac_power_W"
|
||||
in df.columns
|
||||
|
||||
else "ac_power_W"
|
||||
)
|
||||
|
||||
power = df[
|
||||
power_column
|
||||
]
|
||||
|
||||
peak_power = (
|
||||
|
||||
power.max()
|
||||
)
|
||||
|
||||
average_power = (
|
||||
|
||||
power.mean()
|
||||
)
|
||||
|
||||
minimum_power = (
|
||||
|
||||
power.min()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Nominale
|
||||
# --------------------------------------------------------------
|
||||
|
||||
nominal_power = 0.0
|
||||
|
||||
if "nominal_power_W" in df.columns:
|
||||
|
||||
nominal_power = (
|
||||
|
||||
df[
|
||||
"nominal_power_W"
|
||||
].max()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Efficienza
|
||||
# --------------------------------------------------------------
|
||||
|
||||
efficiency = 0.0
|
||||
|
||||
if (
|
||||
|
||||
"efficiency"
|
||||
|
||||
in df.columns
|
||||
):
|
||||
|
||||
efficiency = (
|
||||
|
||||
df[
|
||||
"efficiency"
|
||||
].mean()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Performance Ratio
|
||||
# --------------------------------------------------------------
|
||||
|
||||
performance_ratio = 0.0
|
||||
|
||||
if (
|
||||
|
||||
"performance_ratio"
|
||||
|
||||
in df.columns
|
||||
):
|
||||
|
||||
performance_ratio = (
|
||||
|
||||
df[
|
||||
"performance_ratio"
|
||||
].mean()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Availability
|
||||
# --------------------------------------------------------------
|
||||
|
||||
availability = 0.0
|
||||
|
||||
if (
|
||||
|
||||
"availability"
|
||||
|
||||
in df.columns
|
||||
):
|
||||
|
||||
availability = (
|
||||
|
||||
df[
|
||||
"availability"
|
||||
].mean()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Faults
|
||||
# --------------------------------------------------------------
|
||||
|
||||
faults = self.faults_dataframe()
|
||||
|
||||
fault_count = len(
|
||||
|
||||
faults
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Clipping
|
||||
# --------------------------------------------------------------
|
||||
|
||||
clipping_loss_Wh = 0.0
|
||||
|
||||
if (
|
||||
|
||||
"clipping_loss_W"
|
||||
|
||||
in df.columns
|
||||
):
|
||||
|
||||
clipping_loss_Wh = (
|
||||
|
||||
df[
|
||||
"clipping_loss_W"
|
||||
].sum()
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Specific Yield
|
||||
# --------------------------------------------------------------
|
||||
|
||||
energy_kWh = (
|
||||
|
||||
energy_Wh
|
||||
|
||||
/ 1000.0
|
||||
)
|
||||
|
||||
nominal_kWp = (
|
||||
|
||||
nominal_power
|
||||
|
||||
/ 1000.0
|
||||
)
|
||||
|
||||
specific_yield = (
|
||||
|
||||
safe_divide(
|
||||
|
||||
energy_kWh,
|
||||
|
||||
nominal_kWp
|
||||
)
|
||||
)
|
||||
|
||||
return KPIResult(
|
||||
|
||||
energy_Wh=
|
||||
|
||||
energy_Wh,
|
||||
|
||||
energy_kWh=
|
||||
|
||||
energy_kWh,
|
||||
|
||||
peak_power_W=
|
||||
|
||||
peak_power,
|
||||
|
||||
average_power_W=
|
||||
|
||||
average_power,
|
||||
|
||||
minimum_power_W=
|
||||
|
||||
minimum_power,
|
||||
|
||||
nominal_power_W=
|
||||
|
||||
nominal_power,
|
||||
|
||||
efficiency=
|
||||
|
||||
efficiency,
|
||||
|
||||
performance_ratio=
|
||||
|
||||
performance_ratio,
|
||||
|
||||
availability=
|
||||
|
||||
availability,
|
||||
|
||||
fault_count=
|
||||
|
||||
fault_count,
|
||||
|
||||
clipping_loss_Wh=
|
||||
|
||||
clipping_loss_Wh,
|
||||
|
||||
specific_yield_kWh_kWp=
|
||||
|
||||
specific_yield,
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# GENERIC GROUP KPI
|
||||
# ==================================================================
|
||||
|
||||
def _group_kpi(
|
||||
self,
|
||||
dataframe: pd.DataFrame,
|
||||
group_column: str,
|
||||
power_column: str,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Calcola KPI aggregati per componente.
|
||||
"""
|
||||
|
||||
if dataframe.empty:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
if group_column not in dataframe.columns:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
if power_column not in dataframe.columns:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
grouped = (
|
||||
|
||||
dataframe
|
||||
|
||||
.groupby(
|
||||
|
||||
group_column
|
||||
)
|
||||
|
||||
.agg(
|
||||
|
||||
power_mean_W=(
|
||||
|
||||
power_column,
|
||||
|
||||
"mean"
|
||||
),
|
||||
|
||||
power_peak_W=(
|
||||
|
||||
power_column,
|
||||
|
||||
"max"
|
||||
),
|
||||
|
||||
power_min_W=(
|
||||
|
||||
power_column,
|
||||
|
||||
"min"
|
||||
),
|
||||
|
||||
records=(
|
||||
|
||||
power_column,
|
||||
|
||||
"count"
|
||||
),
|
||||
)
|
||||
|
||||
.reset_index()
|
||||
)
|
||||
|
||||
return grouped
|
||||
|
||||
# ==================================================================
|
||||
# PANEL KPI
|
||||
# ==================================================================
|
||||
|
||||
def panel_kpi(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
KPI per ogni pannello.
|
||||
"""
|
||||
|
||||
df = self.panel_dataframe()
|
||||
|
||||
return self._group_kpi(
|
||||
|
||||
df,
|
||||
|
||||
"panel_id",
|
||||
|
||||
"dc_power_W",
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# COMBINER KPI
|
||||
# ==================================================================
|
||||
|
||||
def combiner_kpi(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
KPI per ogni Combiner Box.
|
||||
"""
|
||||
|
||||
df = self.combiner_dataframe()
|
||||
|
||||
return self._group_kpi(
|
||||
|
||||
df,
|
||||
|
||||
"combiner_id",
|
||||
|
||||
"dc_power_W",
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# INVERTER KPI
|
||||
# ==================================================================
|
||||
|
||||
def inverter_kpi(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
KPI per ogni inverter.
|
||||
"""
|
||||
|
||||
df = self.inverter_dataframe()
|
||||
|
||||
return self._group_kpi(
|
||||
|
||||
df,
|
||||
|
||||
"inverter_id",
|
||||
|
||||
"ac_power_W",
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# DAILY
|
||||
# ==================================================================
|
||||
|
||||
def daily_statistics(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Calcola statistiche giornaliere.
|
||||
"""
|
||||
|
||||
df = self.plant_dataframe()
|
||||
|
||||
if df.empty:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
df = df.copy()
|
||||
|
||||
df[
|
||||
"date"
|
||||
] = df[
|
||||
"timestamp"
|
||||
].dt.date
|
||||
|
||||
aggregation = {
|
||||
|
||||
"ac_power_W":
|
||||
[
|
||||
"mean",
|
||||
"max",
|
||||
],
|
||||
|
||||
"energy_Wh":
|
||||
"sum",
|
||||
}
|
||||
|
||||
valid_columns = {
|
||||
|
||||
key: value
|
||||
|
||||
for key, value
|
||||
in aggregation.items()
|
||||
|
||||
if key in df.columns
|
||||
}
|
||||
|
||||
if not valid_columns:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
return (
|
||||
|
||||
df
|
||||
|
||||
.groupby(
|
||||
|
||||
"date"
|
||||
)
|
||||
|
||||
.agg(
|
||||
|
||||
valid_columns
|
||||
)
|
||||
|
||||
.reset_index()
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# MONTHLY
|
||||
# ==================================================================
|
||||
|
||||
def monthly_statistics(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Calcola statistiche mensili.
|
||||
"""
|
||||
|
||||
df = self.plant_dataframe()
|
||||
|
||||
if df.empty:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
df = df.set_index(
|
||||
|
||||
"timestamp"
|
||||
)
|
||||
|
||||
result = (
|
||||
|
||||
df.resample(
|
||||
|
||||
"ME"
|
||||
)
|
||||
.agg(
|
||||
|
||||
{
|
||||
|
||||
"ac_power_W":
|
||||
|
||||
[
|
||||
"mean",
|
||||
"max",
|
||||
],
|
||||
|
||||
"energy_Wh":
|
||||
|
||||
"sum",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return result.reset_index()
|
||||
|
||||
# ==================================================================
|
||||
# FAULT SUMMARY
|
||||
# ==================================================================
|
||||
|
||||
def fault_summary(
|
||||
self,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Aggrega i fault per livello e componente.
|
||||
"""
|
||||
|
||||
df = self.faults_dataframe()
|
||||
|
||||
if df.empty:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
columns = [
|
||||
|
||||
"component_level",
|
||||
|
||||
"component_id",
|
||||
|
||||
"fault_type",
|
||||
]
|
||||
|
||||
columns = [
|
||||
|
||||
column
|
||||
|
||||
for column
|
||||
in columns
|
||||
|
||||
if column in df.columns
|
||||
]
|
||||
|
||||
if not columns:
|
||||
|
||||
return pd.DataFrame()
|
||||
|
||||
return (
|
||||
|
||||
df
|
||||
|
||||
.groupby(
|
||||
|
||||
columns
|
||||
)
|
||||
|
||||
.size()
|
||||
|
||||
.reset_index(
|
||||
|
||||
name="fault_count"
|
||||
)
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# FULL REPORT
|
||||
# ==================================================================
|
||||
|
||||
def report(
|
||||
self,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Genera un report completo.
|
||||
"""
|
||||
|
||||
plant = self.plant_kpi()
|
||||
|
||||
return {
|
||||
|
||||
"plant": plant.to_dict(),
|
||||
|
||||
"panel_kpi":
|
||||
|
||||
self.panel_kpi(),
|
||||
|
||||
"combiner_kpi":
|
||||
|
||||
self.combiner_kpi(),
|
||||
|
||||
"inverter_kpi":
|
||||
|
||||
self.inverter_kpi(),
|
||||
|
||||
"daily":
|
||||
|
||||
self.daily_statistics(),
|
||||
|
||||
"monthly":
|
||||
|
||||
self.monthly_statistics(),
|
||||
|
||||
"faults":
|
||||
|
||||
self.fault_summary(),
|
||||
}
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# HELPER
|
||||
# ======================================================================
|
||||
|
||||
|
||||
def calculate_statistics(
|
||||
result: SimulationResult,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Helper function.
|
||||
"""
|
||||
|
||||
statistics = SimulationStatistics(
|
||||
|
||||
result
|
||||
)
|
||||
|
||||
return statistics.report()
|
||||
|
||||
|
||||
# ======================================================================
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
"KPIResult",
|
||||
|
||||
"SimulationStatistics",
|
||||
|
||||
"calculate_statistics",
|
||||
]
|
||||
```
|
||||
Reference in New Issue
Block a user