@@ -47,7 +47,7 @@ class Input:
4747 Hazard object to compute impacts from
4848 exposure : climada.Exposures
4949 Exposures object to compute impacts from
50- data : pandas.Dataframe
50+ data : pandas.DataFrame
5151 The data to compare computed impacts to. Index: Event IDs matching the IDs of
5252 :py:attr:`hazard`. Columns: Arbitrary columns. NaN values in the data frame have
5353 special meaning: Corresponding impact values computed by the model are ignored
@@ -64,8 +64,9 @@ class Input:
6464 cost_func : Callable
6565 Function that takes two ``pandas.Dataframe`` objects and returns the scalar
6666 "cost" between them. The optimization algorithm will try to minimize this
67- number. The first argument is the true/correct values (:py:attr:`data`), and the
68- second argument is the estimated/predicted values.
67+ number. The first argument is the true/correct values (:py:attr:`data`), the
68+ second argument is the estimated/predicted values, and the third argument is the
69+ :py:attr:`data_weights`.
6970 bounds : Mapping (str, {Bounds, tuple(float, float)}), optional
7071 The bounds for the parameters. Keys: parameter names. Values:
7172 ``scipy.minimize.Bounds`` instance or tuple of minimum and maximum value.
@@ -85,6 +86,12 @@ class Input:
8586 :py:attr:`data`, insert this value. Defaults to NaN, in which case the impact
8687 from the model is ignored. Set this to zero to explicitly calibrate to zero
8788 impacts in these cases.
89+ data_weights : pandas.DataFrame, optional
90+ Weights for each entry in :py:attr:`data`. Must have the exact same index and
91+ columns. If ``None``, the weights will be ignored (equivalent to the same weight
92+ for each event).
93+ missing_data_value : float, optional
94+ Same as :py:attr:`missing_data_value`, but for :py:attr:`data_weights`.
8895 assign_centroids : bool, optional
8996 If ``True`` (default), assign the hazard centroids to the exposure when this
9097 object is created.
@@ -95,14 +102,16 @@ class Input:
95102 data : pd .DataFrame
96103 impact_func_creator : Callable [..., ImpactFuncSet ]
97104 impact_to_dataframe : Callable [[Impact ], pd .DataFrame ]
98- cost_func : Callable [[pd .DataFrame , pd .DataFrame ], Number ]
105+ cost_func : Callable [[pd .DataFrame , pd .DataFrame , pd . DataFrame | None ], Number ]
99106 bounds : Optional [Mapping [str , Union [Bounds , Tuple [Number , Number ]]]] = None
100107 constraints : Optional [Union [ConstraintType , list [ConstraintType ]]] = None
101108 impact_calc_kwds : Mapping [str , Any ] = field (
102109 default_factory = lambda : {"assign_centroids" : False }
103110 )
104111 missing_data_value : float = np .nan
105- assign_centroids : InitVar [bool ] = True
112+ data_weights : pd .DataFrame | None = field (default = None , kw_only = True )
113+ missing_weights_value : float = field (default = np .nan , kw_only = True )
114+ assign_centroids : InitVar [bool ] = field (default = True , kw_only = True )
106115
107116 def __post_init__ (self , assign_centroids ):
108117 """Prepare input data"""
@@ -115,6 +124,17 @@ def __post_init__(self, assign_centroids):
115124 )
116125 raise TypeError ("'data' must be a pandas.DataFrame" )
117126
127+ if self .data_weights is not None :
128+ try :
129+ pd .testing .assert_index_equal (self .data .index , self .data_weights .index )
130+ pd .testing .assert_index_equal (
131+ self .data .columns , self .data_weights .columns
132+ )
133+ except AssertionError as err :
134+ raise ValueError (
135+ "'data_weights' must have exact same index and columns as 'data'"
136+ ) from err
137+
118138 if assign_centroids :
119139 self .exposure .assign_centroids (self .hazard )
120140
@@ -413,7 +433,9 @@ class Optimizer(ABC):
413433
414434 input : Input
415435
416- def _target_func (self , data : pd .DataFrame , predicted : pd .DataFrame ) -> Number :
436+ def _target_func (
437+ self , data : pd .DataFrame , predicted : pd .DataFrame , weights : pd .DataFrame | None
438+ ) -> Number :
417439 """Target function for the optimizer
418440
419441 The default version of this function simply returns the value of the cost
@@ -427,12 +449,14 @@ def _target_func(self, data: pd.DataFrame, predicted: pd.DataFrame) -> Number:
427449 predicted : pandas.DataFrame
428450 The impact predicted by the data calibration after it has been transformed
429451 into a dataframe by :py:attr:`Input.impact_to_dataframe`.
452+ weights : pandas.DataFrame
453+ The relative weight for each data/entry pair.
430454
431455 Returns
432456 -------
433457 The value of the target function for the optimizer.
434458 """
435- return self .input .cost_func (data , predicted )
459+ return self .input .cost_func (data , predicted , weights )
436460
437461 def _kwargs_to_impact_func_creator (self , * _ , ** kwargs ) -> Dict [str , Any ]:
438462 """Define how the parameters to :py:meth:`_opt_func` must be transformed
@@ -484,11 +508,24 @@ def _opt_func(self, *args, **kwargs) -> Number:
484508 hazard = self .input .hazard ,
485509 ).impact (** self .input .impact_calc_kwds )
486510
487- # Transform to DataFrame, align, and compute target function
511+ # Transform to DataFrame and align
488512 data_aligned , impact_df_aligned = self .input .impact_to_aligned_df (
489- impact , fillna = 0
513+ impact , fillna = 0.0
490514 )
491- return self ._target_func (data_aligned , impact_df_aligned )
515+
516+ # Align weights
517+ weights_aligned = None
518+ if self .input .data_weights is not None :
519+ weights_aligned , _ = self .input .data_weights .align (
520+ data_aligned ,
521+ axis = None ,
522+ join = "right" ,
523+ copy = True ,
524+ fill_value = self .input .missing_weights_value ,
525+ )
526+
527+ # Compute target function
528+ return self ._target_func (data_aligned , impact_df_aligned , weights_aligned )
492529
493530 @abstractmethod
494531 def run (self , ** opt_kwargs ) -> Output :
0 commit comments