data-disaggregation 0.9.3__tar.gz → 0.10.3__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 (23) hide show
  1. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/PKG-INFO +15 -6
  2. data_disaggregation-0.10.3/README.md +13 -0
  3. data_disaggregation-0.10.3/data_disaggregation/__init__.py +5 -0
  4. data_disaggregation-0.10.3/data_disaggregation/actions.py +7 -0
  5. data_disaggregation-0.10.3/data_disaggregation/base.py +185 -0
  6. data_disaggregation-0.10.3/data_disaggregation/ext.py +320 -0
  7. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation/utils.py +54 -34
  8. data_disaggregation-0.10.3/data_disaggregation/vtypes.py +88 -0
  9. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/PKG-INFO +15 -6
  10. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/SOURCES.txt +2 -1
  11. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/setup.py +1 -1
  12. data_disaggregation-0.10.3/test/test.py +518 -0
  13. data-disaggregation-0.9.3/README.md +0 -13
  14. data-disaggregation-0.9.3/data_disaggregation/__init__.py +0 -16
  15. data-disaggregation-0.9.3/data_disaggregation/base.py +0 -168
  16. data-disaggregation-0.9.3/data_disaggregation/classes.py +0 -76
  17. data-disaggregation-0.9.3/data_disaggregation/ext.py +0 -223
  18. data-disaggregation-0.9.3/test/test.py +0 -551
  19. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/LICENSE +0 -0
  20. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/dependency_links.txt +0 -0
  21. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/requires.txt +0 -0
  22. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/top_level.txt +0 -0
  23. {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: data-disaggregation
3
- Version: 0.9.3
3
+ Version: 0.10.3
4
4
  Author: Christian Winger
5
5
  License: MIT
6
6
  Project-URL: Documentation, https://wingechr.github.io/data-disaggregation
@@ -11,13 +11,22 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: pandas
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: license
20
+ Dynamic: platform
21
+ Dynamic: project-url
22
+ Dynamic: requires-dist
14
23
 
15
- # DataDisAggregation
24
+ # README
16
25
 
17
- ![](https://img.shields.io/github/license/wingechr/data-disaggregation)
18
- ![](https://img.shields.io/pypi/pyversions/data-disaggregation)
26
+ [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](./LICENSE)
27
+ [![](https://img.shields.io/pypi/pyversions/data-disaggregation)](https://github.com/wingechr/data-disaggregation)
19
28
  [![](https://img.shields.io/pypi/v/data-disaggregation)](https://pypi.org/project/data-disaggregation/)
20
- ![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/test.yml?label=tests)
29
+ [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/test.yml?label=tests)](https://github.com/wingechr/data-disaggregation/actions/workflows/test.yml)
21
30
  [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/publish.yml?label=docs)](https://wingechr.github.io/data-disaggregation/)
22
31
 
23
32
  ## Install
@@ -0,0 +1,13 @@
1
+ # README
2
+
3
+ [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](./LICENSE)
4
+ [![](https://img.shields.io/pypi/pyversions/data-disaggregation)](https://github.com/wingechr/data-disaggregation)
5
+ [![](https://img.shields.io/pypi/v/data-disaggregation)](https://pypi.org/project/data-disaggregation/)
6
+ [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/test.yml?label=tests)](https://github.com/wingechr/data-disaggregation/actions/workflows/test.yml)
7
+ [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/publish.yml?label=docs)](https://wingechr.github.io/data-disaggregation/)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install data-disaggregation
13
+ ```
@@ -0,0 +1,5 @@
1
+ __version__ = "0.10.3"
2
+
3
+ from . import actions, vtypes
4
+
5
+ __all__ = ["actions", "vtypes"]
@@ -0,0 +1,7 @@
1
+ """Functions to perform data transformations.
2
+ """
3
+
4
+ from .base import transform
5
+ from .ext import transform_pandas
6
+
7
+ __all__ = ["transform", "transform_pandas"]
@@ -0,0 +1,185 @@
1
+ """base code without dependencies.
2
+
3
+ * a Variable is a (dict like) mapping from a Domain -> Value from a given Range
4
+ * a Transformation maps a Variable to a a new Variable in a different Domain
5
+ * Domains can be have multiple, nested dimensions
6
+
7
+ * DomainMap is a Variable that maps (Domain1 x Domain2) to a numerical value (size)
8
+ * DomainSize is a Variable that maps a Domain to a numerical value (size)
9
+ this is only needed for Variables of type MetricExtVarType
10
+
11
+
12
+ Algorithm
13
+
14
+ * We want to map variable of U(Dom1) (of type T) to V(Dom2) (will also be of Type T)
15
+ * Inputs:
16
+ * U(Dom1) and T
17
+ * Dom1Size(Dom1)
18
+ * Dom2Size(Dom2)
19
+ * Dom1Dom2Map(Dom1 x Dom2)
20
+ * Output
21
+ * V(Dom2) of type T
22
+ * Steps
23
+ * start with Dom1Dom2Map, join U (this will replicate values!)
24
+ * IF T==MetricExtVarType:
25
+ * join Dom1Size and Dom2Size and rescale U: U' = U / Dom1Size * Dom2Size
26
+ * GROUP BY Dom2 and use the respective aggregation functions of the type,
27
+ with U being the value and Dom1Dom2Map being the weight
28
+ * Optional Steps
29
+ * if Type is numeric but limited to int: round values
30
+ * if specified: gapfill missing values from Dom2 with na or a suitable default
31
+ * if specified, a threshold for sum(weights)/size(dom2) can be set (usually 0.5)
32
+ to drop elements from output
33
+
34
+ Helper to create the mapping
35
+ * Given a (multidim) input domain and a (multidim) output domain
36
+ * and a weight mapping over a (arbitrary) weight domain:
37
+ * organize into a unique list by shared dims: dims_in, dims_out
38
+ => [dims_in_only] + [dims_shared] + [dims_out_only]
39
+ * weightdin MUST be a subset of this, but MAY have fewers
40
+
41
+ * Steps:
42
+ * create super domain of cross product of all of those
43
+ * join weights
44
+ * create index pairs for result
45
+
46
+ """
47
+
48
+ from typing import Mapping, Tuple
49
+
50
+ from .utils import (
51
+ as_set,
52
+ group_idx_first,
53
+ group_idx_second,
54
+ is_map,
55
+ is_mapping,
56
+ is_na,
57
+ is_subset,
58
+ is_unique,
59
+ iter_values,
60
+ )
61
+ from .vtypes import F, T, V, VariableType, VT_NumericExt
62
+
63
+ VALIDATE_EQ_REL_TOLERANCE = 1e-10
64
+
65
+
66
+ def transform(
67
+ vtype: VariableType,
68
+ data: Mapping[F, V],
69
+ weight_map: Mapping[Tuple[F, T], float],
70
+ weights_from: Mapping[F, float] = None,
71
+ weights_to: Mapping[T, float] = None,
72
+ weight_rel_threshold: float = 0.0,
73
+ validate: bool = True,
74
+ ) -> Mapping[T, V]:
75
+ """(dis-)aggregate data.
76
+
77
+ Parameters
78
+ ----------
79
+ vtype: VariableType
80
+ Variable type of input data, determines the aggregation method.
81
+ data: Mapping[F, V]
82
+ Input data: mapping (usually dict) from any keys (any hashable) to values.
83
+ weight_map: Mapping[Tuple[F, T], float]
84
+ weights for combinations of input and output elements (must be positive).
85
+ Keys must tuples from input/output key pairs.
86
+ weights_from: Mapping[F, float]
87
+ optional weights of input elements (must be positive).
88
+ If not specified, this will be calculated as a sum from `weight_map`.
89
+ weights_to: Mapping[T, float]
90
+ optional weights of output elements (must be positive).
91
+ If not specified, this will be calculated as a sum from `weight_map`.
92
+ weight_rel_threshold: float
93
+ optional value between 0 and 1: all mappings are dropped
94
+ if the sum of input weights / output weight is smaller than this threshold.
95
+ For example, you may want to set it to 0.5 for geographical mappings with
96
+ extensive data.
97
+ validate bool:
98
+ if True: run additional (but costly) validations of weights and data.
99
+
100
+ Returns
101
+ -------
102
+ : Mapping[T, V]
103
+ output data as a mapping from output keys (any hashable) to values.
104
+
105
+ """
106
+ if weights_from is None:
107
+ weights_from = group_idx_first(weight_map)
108
+
109
+ if weights_to is None:
110
+ weights_to = group_idx_second(weight_map)
111
+
112
+ if validate:
113
+ # validate size_f
114
+ assert is_mapping(weights_from)
115
+ assert is_unique(weights_from)
116
+ assert all(v > 0 for v in iter_values(weights_from))
117
+
118
+ # validate size_t
119
+ assert is_mapping(weights_to)
120
+ assert is_unique(weights_to)
121
+ assert all(v > 0 for v in iter_values(weights_to))
122
+
123
+ # validate var
124
+ assert is_mapping(data)
125
+ assert is_unique(data)
126
+
127
+ if not is_subset(data, weights_from):
128
+ err = as_set(data) - as_set(weights_from)
129
+ raise Exception(
130
+ f"Variable index is not a subset of input dimension subset: {err}"
131
+ )
132
+
133
+ # validate map
134
+ assert is_map(weight_map)
135
+ assert is_unique(weight_map)
136
+ assert all(v >= 0 for v in iter_values(weight_map))
137
+ assert is_subset([x[0] for x in weight_map.keys()], weights_from)
138
+ assert is_subset([x[1] for x in weight_map.keys()], weights_to)
139
+ # assert all(isinstance(v, (float, int)) for v in iter_values(weight_map))
140
+
141
+ # filter nan in data
142
+ data = dict((f, v) for f, v in data.items() if not is_na(v))
143
+
144
+ # scale extensive => intensive
145
+ if vtype == VT_NumericExt:
146
+ data = dict((f, v / weights_from[f]) for f, v in data.items())
147
+
148
+ # filter unused in weight_map: input:
149
+ weight_map = dict(((f, t), w) for (f, t), w in weight_map.items() if f in data)
150
+
151
+ # filter unused in weight_map: output
152
+ weight_map = dict(
153
+ ((f, t), w) for (f, t), w in weight_map.items() if weights_to.get(t, 0) > 0
154
+ )
155
+
156
+ # init groups
157
+ groups = dict((t, []) for t in set(_t for (_, _t) in weight_map.keys()))
158
+ # group data by output keys
159
+ for (f, t), w in weight_map.items():
160
+ v = data[f]
161
+ groups[t].append((v, w))
162
+
163
+ # create weight sums
164
+ group_sumw = dict((t, sum(w for _, w in vws)) for t, vws in groups.items())
165
+
166
+ # drop groups under threshold
167
+ if weight_rel_threshold:
168
+ sumw_rel = dict((t, sumw / weights_to[t]) for t, sumw in group_sumw.items())
169
+ groups = dict(
170
+ (t, vws) for t, vws in groups.items() if sumw_rel[t] >= weight_rel_threshold
171
+ )
172
+
173
+ result = {}
174
+ for t, vws in groups.items():
175
+ # normalize weights
176
+ vws = [(v, w / group_sumw[t]) for v, w in vws]
177
+ # aggregate
178
+ result[t] = vtype.weighted_aggregate(vws)
179
+
180
+ # re-scale intensive => extensive
181
+ if vtype == VT_NumericExt:
182
+ for t, v in result.items():
183
+ result[t] = v * weights_to[t]
184
+
185
+ return result
@@ -0,0 +1,320 @@
1
+ """extended functions, especially for pandas Series
2
+ """
3
+
4
+ from typing import List, Tuple, Union
5
+
6
+ import numpy as np
7
+ from pandas import DataFrame, Index, MultiIndex, Series
8
+
9
+ from .base import transform
10
+ from .utils import is_scalar
11
+ from .vtypes import SCALAR_DIM_NAME, SCALAR_INDEX_KEY, VariableType
12
+
13
+ IDX_SCALAR = MultiIndex.from_product([Index([SCALAR_INDEX_KEY], name=SCALAR_DIM_NAME)])
14
+ COL_WEIGHT = "__WEIGHT__"
15
+ COL_FROM = "__FROM__"
16
+ COL_TO = "__TO__"
17
+
18
+
19
+ def harmonize_input_data(data: Union[DataFrame, Series, float]) -> DataFrame:
20
+ """
21
+ return DataFrame with MultiIndex
22
+ """
23
+ if is_scalar(data):
24
+ data = DataFrame({SCALAR_INDEX_KEY: data}, index=IDX_SCALAR)
25
+ elif isinstance(data, Series):
26
+ data = data.to_frame()
27
+ # ensure multiindex
28
+ data = ensure_multiindex(data)
29
+ return data
30
+
31
+
32
+ def as_multiindex(index: Index) -> MultiIndex:
33
+ if not isinstance(index, MultiIndex):
34
+ index = MultiIndex.from_product([index])
35
+ return index
36
+
37
+
38
+ def ensure_multiindex(item: Union[DataFrame, Series]) -> Union[DataFrame, Series]:
39
+ if not isinstance(item.index, MultiIndex):
40
+ index = as_multiindex(item.index)
41
+ item = item.copy() # TODO: can we replace index without copy?
42
+ item.index = index
43
+ return item
44
+
45
+
46
+ def as_list_of_series_w_multiindex(
47
+ items: Union[Index, Series, Tuple[Union[Index, Series]]]
48
+ ) -> List[Series]:
49
+ # make sure we have a list/tuple
50
+ if not isinstance(items, (list, tuple)):
51
+ items = [items]
52
+ # make sure we have series:
53
+ items = [it if isinstance(it, Series) else Series(1, index=it) for it in items]
54
+ items = [ensure_multiindex(it) for it in items]
55
+ return items
56
+
57
+
58
+ def merge_indices(items: List[Union[Series, Index]]) -> MultiIndex:
59
+ """Create product of unions of indices"""
60
+ # ensure items are multiindices
61
+ items = [it if isinstance(it, Index) else it.index for it in items]
62
+ items = [as_multiindex(it) for it in items]
63
+ indices = {}
64
+ for it in items:
65
+ for idx in it.levels:
66
+ if idx.name not in indices:
67
+ indices[idx.name] = idx
68
+ else:
69
+ indices[idx.name] = indices[idx.name].union(idx)
70
+ return MultiIndex.from_product(indices.values())
71
+
72
+
73
+ def combine_weights(
74
+ weights: Union[Index, Series, Tuple[Union[Index, Series]]]
75
+ ) -> Series:
76
+ """multiply all weights series
77
+ * join on overlapping columns (or all if none
78
+ * if index and not series: use value 1
79
+
80
+ Returns:
81
+ Series with MultiIndex
82
+ """
83
+ # make sure we have series:
84
+ weights = as_list_of_series_w_multiindex(weights)
85
+
86
+ # merge indices
87
+ idx = merge_indices(weights)
88
+
89
+ # multiply all and drop nan
90
+ result = Series(1, index=idx)
91
+ for w in weights:
92
+ # IMPORTANT: `result *= w` gives a different result, so DONT use it
93
+ result = result * w
94
+
95
+ # multiplications implicit join can change order:
96
+ if isinstance(result.index, MultiIndex):
97
+ result.index = result.index.reorder_levels(idx.names)
98
+
99
+ # drop nan
100
+ result.dropna(inplace=True)
101
+ return result
102
+
103
+
104
+ def format_result(df, input_is_df, output_is_scalar, output_multiindex):
105
+ if not output_multiindex:
106
+ assert len(df.index.levels) == 1
107
+ df = df.set_index(df.index.levels[0])
108
+ if input_is_df:
109
+ if output_is_scalar:
110
+ # return series instead of Frame
111
+ return df.iloc[0, :]
112
+ else:
113
+ return df.iloc[:, :]
114
+ else:
115
+ if output_is_scalar:
116
+ # return scalar
117
+ return df.iloc[0, 0]
118
+ else:
119
+ return df.iloc[:, 0]
120
+
121
+
122
+ def get_idx_out(idx_in: MultiIndex, idx_weights: MultiIndex) -> MultiIndex:
123
+ idx_all = merge_indices([idx_in, idx_weights])
124
+ idx_levels = dict(zip(idx_all.names, idx_all.levels))
125
+
126
+ idx_names_only_in = set(idx_in.names) - set(idx_weights.names)
127
+ idx_names_only_weights = set(idx_weights.names) - set(idx_in.names)
128
+ # idx_names_both = set(idx_weights.names) & set(idx_in.names)
129
+
130
+ # TODO give user options
131
+ # symmetric difference
132
+ idx_names_out = idx_names_only_in | idx_names_only_weights
133
+
134
+ idx_out = MultiIndex.from_product([idx_levels[n] for n in idx_names_out])
135
+
136
+ return idx_out
137
+
138
+
139
+ def remap_series_to_frame(s: Series, idx: MultiIndex, colname: str) -> DataFrame:
140
+ assert set(s.index.names) <= set(idx.names)
141
+ # create sub-index from idx: only columns that are in series
142
+ # TODO: better way?
143
+ df_result = DataFrame({colname: np.nan}, index=idx)
144
+ # convert index into columns
145
+ df_result.reset_index(inplace=True)
146
+ # set index to same levels as `s`, but keep columns
147
+
148
+ df_result.set_index(s.index.names, drop=False, inplace=True)
149
+ df_result = ensure_multiindex(df_result)
150
+
151
+ # join in data
152
+ df_result[colname] = s
153
+ df_result.reset_index(inplace=True, drop=True)
154
+
155
+ return df_result
156
+
157
+
158
+ def create_weight_map(
159
+ ds_weights: Series, idx_in: MultiIndex, idx_out: MultiIndex
160
+ ) -> Series:
161
+ """
162
+ Returns weight Series
163
+ Index is 2 dimensional (F, T), each part is a tuple from idx_in, idx_out
164
+ for overlapping levels: left == right
165
+
166
+ """
167
+ idx_all = merge_indices([idx_in, idx_out])
168
+ # expand index (TODO: check if weights are dropped??)
169
+ df = remap_series_to_frame(ds_weights, idx_all, COL_WEIGHT)
170
+ df[COL_FROM] = list(zip(*[df[n] for n in idx_in.names]))
171
+ df[COL_TO] = list(zip(*[df[n] for n in idx_out.names]))
172
+
173
+ # filter: TODO, faster way?
174
+ df = df.loc[df[COL_FROM].isin(idx_in)]
175
+ df = df.loc[df[COL_TO].isin(idx_out)]
176
+
177
+ ds_weight_map = df.set_index([COL_FROM, COL_TO])[COL_WEIGHT]
178
+ ds_weight_map = ds_weight_map.fillna(0)
179
+
180
+ return ds_weight_map
181
+
182
+
183
+ def validate_multiindex(item: Union[Index, Series, DataFrame]):
184
+ if isinstance(item, (Series, DataFrame)):
185
+ item = item.index
186
+ assert isinstance(item, MultiIndex)
187
+ assert all(item.names) # no empty names
188
+ assert len(set(item.names)) == len(item.names) # unique names
189
+ assert item.is_unique # unique values
190
+
191
+
192
+ def transform_pandas(
193
+ vtype: VariableType,
194
+ data: Union[DataFrame, Series, float],
195
+ weights: Union[Index, Series, Tuple[Union[Index, Series]]],
196
+ dim_in: Union[Index, Series] = None,
197
+ dim_out: Union[Index, Series] = None,
198
+ validate: bool = True,
199
+ ) -> Union[DataFrame, Series, float]:
200
+ """(dis-)aggregate data (pandas).
201
+
202
+ Parameters
203
+ ----------
204
+ vtype : VariableType
205
+ Variable type of input data, determines the aggregation method.
206
+ data : Union[DataFrame, Series, float]
207
+ Input data. Indices must be unique and have unique level names
208
+ weights : Union[Index, Series, Tuple[Union[Index, Series]]]
209
+ weights for combinations of input and output elements (must be positive).
210
+ dim_in : Union[Index, Series], optional
211
+ TODO
212
+ dim_out : Union[Index, Series], optional
213
+ TODO
214
+ validate : bool, optional
215
+ if True: run additional (but costly) validations of weights and data.
216
+
217
+ Returns
218
+ -------
219
+ Union[DataFrame, Series, float]
220
+ output data
221
+
222
+ """
223
+
224
+ # ensure data is DataFrame with MultiIndex
225
+ df_data = harmonize_input_data(data)
226
+
227
+ if validate:
228
+ validate_multiindex(df_data)
229
+
230
+ # combine weights into single Series with MultiIndex
231
+ ds_weights = combine_weights(weights)
232
+
233
+ if validate:
234
+ validate_multiindex(ds_weights)
235
+
236
+ # determine input index
237
+ if dim_in is None:
238
+ idx_in = df_data.index
239
+ ds_size_in = None
240
+ elif isinstance(dim_in, Index):
241
+ idx_in = as_multiindex(dim_in)
242
+ ds_size_in = None
243
+ else:
244
+ ds_size_in = ensure_multiindex(dim_in)
245
+ idx_in = ds_size_in.index
246
+
247
+ # determine output_index
248
+ if dim_out is None:
249
+ idx_out = get_idx_out(idx_in, ds_weights.index)
250
+ ds_size_out = None
251
+ elif isinstance(dim_out, Index):
252
+ idx_out = as_multiindex(dim_out)
253
+ ds_size_out = None
254
+ else:
255
+ ds_size_out = ensure_multiindex(dim_out)
256
+ idx_out = ds_size_out.index
257
+
258
+ if validate:
259
+ validate_multiindex(idx_in)
260
+ validate_multiindex(idx_out)
261
+
262
+ # create weight map
263
+ ds_weight_map = create_weight_map(ds_weights, idx_in, idx_out)
264
+
265
+ if ds_size_in is None:
266
+ ds_size_in = (
267
+ ds_weight_map.reset_index().groupby(COL_FROM).sum(COL_WEIGHT)[COL_WEIGHT]
268
+ )
269
+ # fix index
270
+ ds_size_in.index = MultiIndex.from_tuples(
271
+ ds_size_in.index.values, names=idx_in.names
272
+ )
273
+ ds_size_in = ds_size_in.loc[ds_size_in > 0]
274
+
275
+ if ds_size_out is None:
276
+ ds_size_out = (
277
+ ds_weight_map.reset_index().groupby(COL_TO).sum(COL_WEIGHT)[COL_WEIGHT]
278
+ )
279
+ # fix index
280
+ ds_size_out.index = MultiIndex.from_tuples(
281
+ ds_size_out.index.values, names=idx_out.names
282
+ )
283
+ ds_size_out = ds_size_out.loc[ds_size_out > 0]
284
+
285
+ if validate:
286
+ validate_multiindex(ds_size_in)
287
+ validate_multiindex(ds_size_out)
288
+ assert ds_size_in.index.isin(idx_in).all()
289
+ assert ds_size_out.index.isin(idx_out).all()
290
+
291
+ idx_in = ds_size_in.index
292
+ idx_out = ds_size_out.index
293
+
294
+ # apply base function
295
+ df_result = DataFrame(index=idx_out)
296
+ for name in df_data.columns:
297
+ s_col = df_data[name]
298
+ s_col = s_col.dropna()
299
+
300
+ res_col = transform(
301
+ vtype=vtype,
302
+ data=s_col,
303
+ weight_map=ds_weight_map,
304
+ weights_from=ds_size_in,
305
+ weights_to=ds_size_out,
306
+ weight_rel_threshold=0.0,
307
+ validate=False, # we do validation in pandas
308
+ )
309
+
310
+ s_res_col = Series(res_col, name=s_col.name)
311
+ df_result[s_col.name] = s_res_col
312
+
313
+ return format_result(
314
+ df_result,
315
+ input_is_df=isinstance(data, DataFrame),
316
+ output_is_scalar=idx_out.equals(IDX_SCALAR),
317
+ output_multiindex=(
318
+ True if dim_out is None else isinstance(idx_out, MultiIndex)
319
+ ),
320
+ )