data-disaggregation 0.11.0__tar.gz → 0.12.0__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 (22) hide show
  1. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/PKG-INFO +14 -14
  2. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/README.md +9 -2
  3. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation/__init__.py +1 -2
  4. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation/actions.py +1 -2
  5. data_disaggregation-0.12.0/data_disaggregation/base.py +263 -0
  6. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation/ext.py +68 -68
  7. data_disaggregation-0.12.0/data_disaggregation/utils.py +112 -0
  8. data_disaggregation-0.12.0/data_disaggregation/vtypes.py +188 -0
  9. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation.egg-info/PKG-INFO +14 -14
  10. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation.egg-info/SOURCES.txt +2 -2
  11. data_disaggregation-0.12.0/data_disaggregation.egg-info/requires.txt +1 -0
  12. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation.egg-info/top_level.txt +1 -0
  13. data_disaggregation-0.12.0/pyproject.toml +86 -0
  14. data_disaggregation-0.11.0/test/test.py → data_disaggregation-0.12.0/tests/test_data_disaggregation.py +29 -77
  15. data_disaggregation-0.11.0/data_disaggregation/base.py +0 -185
  16. data_disaggregation-0.11.0/data_disaggregation/utils.py +0 -196
  17. data_disaggregation-0.11.0/data_disaggregation/vtypes.py +0 -88
  18. data_disaggregation-0.11.0/data_disaggregation.egg-info/requires.txt +0 -1
  19. data_disaggregation-0.11.0/setup.py +0 -28
  20. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/LICENSE +0 -0
  21. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/data_disaggregation.egg-info/dependency_links.txt +0 -0
  22. {data_disaggregation-0.11.0 → data_disaggregation-0.12.0}/setup.cfg +0 -0
@@ -1,34 +1,34 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: data-disaggregation
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Author: Christian Winger
5
5
  License: MIT
6
6
  Project-URL: Documentation, https://wingechr.github.io/data-disaggregation
7
7
  Project-URL: Source, https://github.com/wingechr/data-disaggregation
8
- Platform: any
9
8
  Classifier: Programming Language :: Python :: 3
10
9
  Classifier: License :: OSI Approved :: MIT License
11
10
  Classifier: Operating System :: OS Independent
11
+ Requires-Python: <3.15,>=3.10
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
+ Requires-Dist: pandas>=2.0
15
+ Dynamic: license-file
23
16
 
24
- # README
17
+ # DataDisAggregation Package
25
18
 
26
- [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](./LICENSE)
19
+ [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](https://github.com/wingechr/data-disaggregation/blob/main/LICENSE)
27
20
  [![](https://img.shields.io/pypi/pyversions/data-disaggregation)](https://github.com/wingechr/data-disaggregation)
28
21
  [![](https://img.shields.io/pypi/v/data-disaggregation)](https://pypi.org/project/data-disaggregation/)
29
22
  [![](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)
30
23
  [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/publish.yml?label=docs)](https://wingechr.github.io/data-disaggregation/)
31
24
 
25
+ DataDisAggregation is a python package that lets you perform weighted aggregations and
26
+ weighted disaggregations of data.
27
+
28
+ Development was funded as part of the IND-E project.
29
+
30
+ ![](static/BMWK_Fz_2017_Web2x_en.gif)
31
+
32
32
  ## Install
33
33
 
34
34
  ```bash
@@ -1,11 +1,18 @@
1
- # README
1
+ # DataDisAggregation Package
2
2
 
3
- [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](./LICENSE)
3
+ [![](https://img.shields.io/github/license/wingechr/data-disaggregation)](https://github.com/wingechr/data-disaggregation/blob/main/LICENSE)
4
4
  [![](https://img.shields.io/pypi/pyversions/data-disaggregation)](https://github.com/wingechr/data-disaggregation)
5
5
  [![](https://img.shields.io/pypi/v/data-disaggregation)](https://pypi.org/project/data-disaggregation/)
6
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
7
  [![](https://img.shields.io/github/actions/workflow/status/wingechr/data-disaggregation/publish.yml?label=docs)](https://wingechr.github.io/data-disaggregation/)
8
8
 
9
+ DataDisAggregation is a python package that lets you perform weighted aggregations and
10
+ weighted disaggregations of data.
11
+
12
+ Development was funded as part of the IND-E project.
13
+
14
+ ![](static/BMWK_Fz_2017_Web2x_en.gif)
15
+
9
16
  ## Install
10
17
 
11
18
  ```bash
@@ -1,5 +1,4 @@
1
- __version__ = "0.11.0"
2
-
3
1
  from . import actions, vtypes
4
2
 
3
+ __version__ = "0.12.0"
5
4
  __all__ = ["actions", "vtypes"]
@@ -1,5 +1,4 @@
1
- """Functions to perform data transformations.
2
- """
1
+ """Functions to perform data transformations."""
3
2
 
4
3
  from .base import transform
5
4
  from .ext import transform_pandas
@@ -0,0 +1,263 @@
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 __future__ import annotations # Series[...] for older python/pandas
49
+
50
+ from typing import Any
51
+
52
+ import pandas as pd
53
+ from pandas import DataFrame, Index, MultiIndex, Series
54
+
55
+ from .utils import SeriesDict, SeriesFrame, as_series
56
+ from .vtypes import VariableType
57
+
58
+ _NA_DIM_KEY = "__NA__"
59
+
60
+
61
+ def get_NA_DIM_KEY(for_index: Index) -> Any:
62
+ sample = for_index[0]
63
+ if isinstance(sample, tuple):
64
+ return tuple([_NA_DIM_KEY] * len(sample))
65
+ return _NA_DIM_KEY
66
+
67
+
68
+ def _assert_index_unique_no_na(index: Index, name: str):
69
+ if not index.is_unique:
70
+ raise Exception(f"index not unique in {name}: {set(index.duplicated())}")
71
+ for level in range(index.nlevels):
72
+ if any(index.get_level_values(level).isna()):
73
+ raise Exception(f"index contains NA in {name}")
74
+ if get_NA_DIM_KEY(index) in index:
75
+ raise Exception(f"index contains {get_NA_DIM_KEY(index)} in {name}")
76
+
77
+
78
+ def _assert_all_gt0(data: SeriesFrame, name: str):
79
+ if not all(data > 0):
80
+ raise Exception(f"Not all values are > 0 in {name}")
81
+
82
+
83
+ def _assert_all_ge0(data: SeriesFrame, name: str):
84
+ if not all(data >= 0):
85
+ raise Exception(f"Not all values are >= 0 in {name}")
86
+
87
+
88
+ def _assert_index_subset(index: Index, ref_index: Index, name: str):
89
+ err = set(index) - set(ref_index)
90
+ if err:
91
+ raise Exception(f"Unexepcted, additional elements in {name}: {err}")
92
+
93
+
94
+ def _check_initialize_weights(
95
+ df_weight_map: DataFrame,
96
+ ds_weights_from: Series[float],
97
+ ds_weights_to: Series[float],
98
+ ) -> tuple[DataFrame, Series, Series]:
99
+ # convert weights to frame, rows = from, cols = to, realign
100
+ df_weight_map = df_weight_map.reindex(
101
+ index=ds_weights_from.index, columns=ds_weights_to.index
102
+ ).fillna(0)
103
+
104
+ # all indices must be unique and not NA
105
+ _assert_index_unique_no_na(df_weight_map.index, "weights (source)")
106
+ _assert_index_unique_no_na(df_weight_map.columns, "weights (target)")
107
+ _assert_index_unique_no_na(ds_weights_from.index, "weights (sum source)")
108
+ _assert_index_unique_no_na(ds_weights_to.index, "weights (sum target)")
109
+ _assert_all_gt0(ds_weights_from, "weights (sum source)")
110
+ _assert_all_gt0(ds_weights_to, "weights (sum target)")
111
+ _assert_all_ge0(df_weight_map, "weights")
112
+ # weights have to be in ds_weights_from / ds_weights_to sums
113
+ _assert_index_subset(df_weight_map.index, ds_weights_from.index, "weights (source)")
114
+ _assert_index_subset(df_weight_map.columns, ds_weights_to.index, "weights (target)")
115
+
116
+ # differences of sum weights and weigh sums
117
+ ds_weights_from_rest = ds_weights_from - df_weight_map.sum(axis=1)
118
+ _assert_all_ge0(ds_weights_from_rest, "col weights for NA")
119
+ ds_weights_to_rest = ds_weights_to - df_weight_map.sum(axis=0)
120
+ _assert_all_ge0(ds_weights_to_rest, "row weights for NA")
121
+
122
+ # add additional row for NA in source
123
+ df_weight_map = pd.concat(
124
+ [
125
+ df_weight_map,
126
+ ds_weights_to_rest.to_frame().T.set_axis(
127
+ [get_NA_DIM_KEY(df_weight_map.index)]
128
+ ),
129
+ ]
130
+ )
131
+ ds_weights_from_rest = pd.concat(
132
+ [
133
+ ds_weights_from_rest,
134
+ Series(0, index=[get_NA_DIM_KEY(ds_weights_from_rest.index)]),
135
+ ]
136
+ )
137
+
138
+ # add NA output col (temp)
139
+ df_weight_map_w_na_column = pd.concat(
140
+ [
141
+ df_weight_map,
142
+ ds_weights_from_rest.rename(
143
+ get_NA_DIM_KEY(df_weight_map.columns) # type: ignore (yes, NA should be col identifier)
144
+ ),
145
+ ],
146
+ axis=1,
147
+ )
148
+
149
+ # df_weight_map.columns = ds_weights_to.index
150
+ df_weight_map.columns.names = ds_weights_to.index.names
151
+
152
+ ds_weights_from = df_weight_map_w_na_column.sum(axis=1)
153
+ ds_weights_to = df_weight_map.sum(axis=0)
154
+
155
+ return df_weight_map, ds_weights_from, ds_weights_to
156
+
157
+
158
+ class Transformer:
159
+ def __init__(
160
+ self,
161
+ vtype: type[VariableType],
162
+ df_weight_map: DataFrame,
163
+ ds_weights_from: Series | None = None,
164
+ ds_weights_to: Series | None = None,
165
+ weight_rel_threshold: float = 0.0,
166
+ ):
167
+ ds_weights_from = (
168
+ df_weight_map.sum(axis=1) if ds_weights_from is None else ds_weights_from
169
+ )
170
+ ds_weights_to = (
171
+ df_weight_map.sum(axis=0) if ds_weights_to is None else ds_weights_to
172
+ )
173
+ df_weight_map, ds_weights_from, ds_weights_to = _check_initialize_weights(
174
+ df_weight_map, ds_weights_from, ds_weights_to
175
+ )
176
+
177
+ self.df_weight_map: DataFrame = df_weight_map
178
+ self.ds_weights_from: Series = ds_weights_from
179
+ self.ds_weights_to: Series = ds_weights_to
180
+ self.vtype: type[VariableType] = vtype
181
+ self.weight_rel_threshold: float = weight_rel_threshold
182
+
183
+ def __call__(self, ds_data: Series) -> Series:
184
+ _assert_index_unique_no_na(ds_data.index, str(ds_data.name))
185
+ return (
186
+ self.vtype.transform(
187
+ ds_data=ds_data,
188
+ df_weight_map=self.df_weight_map,
189
+ ds_weights_from=self.ds_weights_from,
190
+ # ds_weights_to=self.ds_weights_to,
191
+ weight_rel_threshold=self.weight_rel_threshold,
192
+ )
193
+ .rename(ds_data.name) # type:ignore
194
+ .rename_axis(index=self.df_weight_map.columns.names)
195
+ ) # type:ignore
196
+
197
+
198
+ def transform(
199
+ vtype: type[VariableType],
200
+ data: SeriesDict,
201
+ weight_map: SeriesDict,
202
+ weights_from: SeriesDict | None = None,
203
+ weights_to: SeriesDict | None = None,
204
+ weight_rel_threshold: float = 0.0,
205
+ ) -> SeriesDict:
206
+ """(dis-)aggregate data.
207
+
208
+ Parameters
209
+ ----------
210
+ vtype: VariableType
211
+ Variable type of input data, determines the aggregation method.
212
+ data: Mapping[F, V]
213
+ Input data: mapping (usually dict) from any keys (any hashable) to values.
214
+ weight_map: Mapping[Tuple[F, T], float]
215
+ weights for combinations of input and output elements (must be positive).
216
+ Keys must tuples from input/output key pairs.
217
+ weights_from: Mapping[F, float]
218
+ optional weights of input elements (must be positive).
219
+ If not specified, this will be calculated as a sum from `weight_map`.
220
+ weights_to: Mapping[T, float]
221
+ optional weights of output elements (must be positive).
222
+ If not specified, this will be calculated as a sum from `weight_map`.
223
+ weight_rel_threshold: float
224
+ optional value between 0 and 1: all mappings are dropped
225
+ if the sum of input weights / output weight is smaller than this threshold.
226
+ For example, you may want to set it to 0.5 for geographical mappings with
227
+ extensive data.
228
+
229
+ Returns
230
+ -------
231
+ : Mapping[T, V]
232
+ output data as a mapping from output keys (any hashable) to values.
233
+
234
+ """
235
+
236
+ ds_data = as_series(data)
237
+ # harmonize NaN/None/NA and drop from data
238
+ ds_data = ds_data.convert_dtypes().dropna() # normalize and drop na
239
+
240
+ ds_weight_map = as_series(weight_map)
241
+ df_weight_map = ds_weight_map.unstack(level=1)
242
+ # fix problem with multiindex
243
+ if isinstance(ds_data.index, MultiIndex) and not isinstance(
244
+ df_weight_map.index, MultiIndex
245
+ ):
246
+ df_weight_map.index = pd.MultiIndex.from_tuples(df_weight_map.index)
247
+
248
+ ds_weights_from = None if weights_from is None else as_series(weights_from)
249
+ ds_weights_to = None if weights_to is None else as_series(weights_to)
250
+
251
+ transformer = Transformer(
252
+ vtype=vtype,
253
+ df_weight_map=df_weight_map,
254
+ ds_weights_from=ds_weights_from,
255
+ ds_weights_to=ds_weights_to,
256
+ weight_rel_threshold=weight_rel_threshold,
257
+ )
258
+
259
+ ds_result = transformer(ds_data)
260
+
261
+ result = type(data)(ds_result)
262
+
263
+ return result
@@ -1,14 +1,13 @@
1
- """extended functions, especially for pandas Series
2
- """
1
+ """extended functions, especially for pandas Series"""
3
2
 
4
- from typing import List, Tuple, Union
3
+ from typing import cast
5
4
 
6
5
  import numpy as np
7
6
  from pandas import DataFrame, Index, MultiIndex, Series
8
7
 
9
- from .base import transform
10
- from .utils import is_scalar
11
- from .vtypes import SCALAR_DIM_NAME, SCALAR_INDEX_KEY, VariableType
8
+ from .base import Transformer
9
+ from .utils import SCALAR_DIM_NAME, SCALAR_INDEX_KEY, SeriesFrame, is_scalar
10
+ from .vtypes import VariableType
12
11
 
13
12
  IDX_SCALAR = MultiIndex.from_product([Index([SCALAR_INDEX_KEY], name=SCALAR_DIM_NAME)])
14
13
  COL_WEIGHT = "__WEIGHT__"
@@ -16,17 +15,17 @@ COL_FROM = "__FROM__"
16
15
  COL_TO = "__TO__"
17
16
 
18
17
 
19
- def harmonize_input_data(data: Union[DataFrame, Series, float]) -> DataFrame:
20
- """
21
- return DataFrame with MultiIndex
22
- """
18
+ def harmonize_input_data(data: DataFrame | Series | float) -> DataFrame:
19
+ """return DataFrame with MultiIndex"""
23
20
  if is_scalar(data):
24
- data = DataFrame({SCALAR_INDEX_KEY: data}, index=IDX_SCALAR)
21
+ df = DataFrame({SCALAR_INDEX_KEY: data}, index=IDX_SCALAR)
25
22
  elif isinstance(data, Series):
26
- data = data.to_frame()
23
+ df = data.to_frame()
24
+ elif not isinstance(data, DataFrame):
25
+ raise TypeError(type(data))
27
26
  # ensure multiindex
28
- data = ensure_multiindex(data)
29
- return data
27
+ df = ensure_multiindex(df)
28
+ return df
30
29
 
31
30
 
32
31
  def as_multiindex(index: Index) -> MultiIndex:
@@ -35,7 +34,7 @@ def as_multiindex(index: Index) -> MultiIndex:
35
34
  return index
36
35
 
37
36
 
38
- def ensure_multiindex(item: Union[DataFrame, Series]) -> Union[DataFrame, Series]:
37
+ def ensure_multiindex(item: SeriesFrame) -> SeriesFrame:
39
38
  if not isinstance(item.index, MultiIndex):
40
39
  index = as_multiindex(item.index)
41
40
  item = item.copy() # TODO: can we replace index without copy?
@@ -44,51 +43,56 @@ def ensure_multiindex(item: Union[DataFrame, Series]) -> Union[DataFrame, Series
44
43
 
45
44
 
46
45
  def as_list_of_series_w_multiindex(
47
- items: Union[Index, Series, Tuple[Union[Index, Series]]]
48
- ) -> List[Series]:
46
+ items: Index | Series | list[Index | Series],
47
+ ) -> list[Series]:
49
48
  # make sure we have a list/tuple
50
- if not isinstance(items, (list, tuple)):
51
- items = [items]
49
+ list_items = [items] if not isinstance(items, (list, tuple)) else items
52
50
  # 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
51
+ list_items = [
52
+ it if isinstance(it, Series) else Series(1, index=it) for it in list_items
53
+ ]
54
+ list_items = [ensure_multiindex(it) for it in list_items]
55
+ return list_items
56
56
 
57
57
 
58
- def merge_indices(items: List[Union[Series, Index]]) -> MultiIndex:
58
+ def merge_indices(
59
+ items: list[Series] | list[Index] | list[Index | Series],
60
+ ) -> MultiIndex:
59
61
  """Create product of unions of indices"""
60
62
  # 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
+ list_items = [it if isinstance(it, Index) else it.index for it in items]
64
+ list_items = [as_multiindex(it) for it in list_items]
63
65
  indices = {}
64
- for it in items:
66
+ for it in list_items:
65
67
  for idx in it.levels:
66
68
  if idx.name not in indices:
67
69
  indices[idx.name] = idx
68
70
  else:
69
71
  indices[idx.name] = indices[idx.name].union(idx)
70
- return MultiIndex.from_product(indices.values())
72
+ return MultiIndex.from_product(list(indices.values()))
71
73
 
72
74
 
73
75
  def combine_weights(
74
- weights: Union[Index, Series, Tuple[Union[Index, Series]]]
76
+ weights: Index | Series | list[Index | Series],
75
77
  ) -> Series:
76
78
  """multiply all weights series
79
+
77
80
  * join on overlapping columns (or all if none
78
81
  * if index and not series: use value 1
79
82
 
80
83
  Returns:
81
84
  Series with MultiIndex
85
+
82
86
  """
83
87
  # make sure we have series:
84
- weights = as_list_of_series_w_multiindex(weights)
88
+ weights_list = as_list_of_series_w_multiindex(weights)
85
89
 
86
90
  # merge indices
87
- idx = merge_indices(weights)
91
+ idx = merge_indices(weights_list)
88
92
 
89
93
  # multiply all and drop nan
90
94
  result = Series(1, index=idx)
91
- for w in weights:
95
+ for w in weights_list:
92
96
  # IMPORTANT: `result *= w` gives a different result, so DONT use it
93
97
  result = result * w
94
98
 
@@ -121,7 +125,7 @@ def format_result(df, input_is_df, output_is_scalar, output_multiindex):
121
125
 
122
126
  def get_idx_out(idx_in: MultiIndex, idx_weights: MultiIndex) -> MultiIndex:
123
127
  idx_all = merge_indices([idx_in, idx_weights])
124
- idx_levels = dict(zip(idx_all.names, idx_all.levels))
128
+ idx_levels = dict(zip(idx_all.names, idx_all.levels, strict=False))
125
129
 
126
130
  idx_names_only_in = set(idx_in.names) - set(idx_weights.names)
127
131
  idx_names_only_weights = set(idx_weights.names) - set(idx_in.names)
@@ -158,8 +162,8 @@ def remap_series_to_frame(s: Series, idx: MultiIndex, colname: str) -> DataFrame
158
162
  def create_weight_map(
159
163
  ds_weights: Series, idx_in: MultiIndex, idx_out: MultiIndex
160
164
  ) -> Series:
161
- """
162
- Returns weight Series
165
+ """Returns weight Series
166
+
163
167
  Index is 2 dimensional (F, T), each part is a tuple from idx_in, idx_out
164
168
  for overlapping levels: left == right
165
169
 
@@ -167,8 +171,8 @@ def create_weight_map(
167
171
  idx_all = merge_indices([idx_in, idx_out])
168
172
  # expand index (TODO: check if weights are dropped??)
169
173
  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]))
174
+ df[COL_FROM] = list(zip(*[df[n] for n in idx_in.names], strict=False))
175
+ df[COL_TO] = list(zip(*[df[n] for n in idx_out.names], strict=False))
172
176
 
173
177
  # filter: TODO, faster way?
174
178
  df = df.loc[df[COL_FROM].isin(idx_in)]
@@ -180,7 +184,7 @@ def create_weight_map(
180
184
  return ds_weight_map
181
185
 
182
186
 
183
- def validate_multiindex(item: Union[Index, Series, DataFrame]):
187
+ def validate_multiindex(item: Index | Series | DataFrame):
184
188
  if isinstance(item, (Series, DataFrame)):
185
189
  item = item.index
186
190
  assert isinstance(item, MultiIndex)
@@ -190,13 +194,13 @@ def validate_multiindex(item: Union[Index, Series, DataFrame]):
190
194
 
191
195
 
192
196
  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,
197
+ vtype: type[VariableType],
198
+ data: DataFrame | Series | float,
199
+ weights: Index | Series | list[Index | Series],
200
+ dim_in: Index | Series | None = None,
201
+ dim_out: Index | Series | None = None,
198
202
  validate: bool = True,
199
- ) -> Union[DataFrame, Series, float]:
203
+ ) -> DataFrame | Series | float:
200
204
  """(dis-)aggregate data (pandas).
201
205
 
202
206
  Parameters
@@ -244,9 +248,12 @@ def transform_pandas(
244
248
  ds_size_in = ensure_multiindex(dim_in)
245
249
  idx_in = ds_size_in.index
246
250
 
251
+ idx_in = cast(MultiIndex, idx_in)
252
+
247
253
  # determine output_index
248
254
  if dim_out is None:
249
- idx_out = get_idx_out(idx_in, ds_weights.index)
255
+ idx_weights = cast(MultiIndex, ds_weights.index)
256
+ idx_out = get_idx_out(idx_in, idx_weights)
250
257
  ds_size_out = None
251
258
  elif isinstance(dim_out, Index):
252
259
  idx_out = as_multiindex(dim_out)
@@ -254,6 +261,7 @@ def transform_pandas(
254
261
  else:
255
262
  ds_size_out = ensure_multiindex(dim_out)
256
263
  idx_out = ds_size_out.index
264
+ idx_out = cast(MultiIndex, idx_out)
257
265
 
258
266
  if validate:
259
267
  validate_multiindex(idx_in)
@@ -263,9 +271,7 @@ def transform_pandas(
263
271
  ds_weight_map = create_weight_map(ds_weights, idx_in, idx_out)
264
272
 
265
273
  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
- )
274
+ ds_size_in = ds_weight_map.reset_index().groupby(COL_FROM).sum()[COL_WEIGHT]
269
275
  # fix index
270
276
  ds_size_in.index = MultiIndex.from_tuples(
271
277
  ds_size_in.index.values, names=idx_in.names
@@ -273,9 +279,7 @@ def transform_pandas(
273
279
  ds_size_in = ds_size_in.loc[ds_size_in > 0]
274
280
 
275
281
  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
- )
282
+ ds_size_out = ds_weight_map.reset_index().groupby(COL_TO).sum()[COL_WEIGHT]
279
283
  # fix index
280
284
  ds_size_out.index = MultiIndex.from_tuples(
281
285
  ds_size_out.index.values, names=idx_out.names
@@ -291,26 +295,20 @@ def transform_pandas(
291
295
  idx_in = ds_size_in.index
292
296
  idx_out = ds_size_out.index
293
297
 
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
- )
298
+ df_weight_map = ds_weight_map.unstack(level=1)
299
+ df_weight_map.columns = idx_out
309
300
 
310
- s_res_col = Series(res_col, name=s_col.name)
311
- df_result[s_col.name] = s_res_col
301
+ transformer = Transformer(
302
+ vtype=vtype,
303
+ df_weight_map=df_weight_map,
304
+ ds_weights_from=ds_size_in,
305
+ ds_weights_to=ds_size_out,
306
+ weight_rel_threshold=0,
307
+ )
308
+
309
+ df_result = df_data.apply(transformer) # FIXME: why is this different
312
310
 
313
- return format_result(
311
+ result = format_result(
314
312
  df_result,
315
313
  input_is_df=isinstance(data, DataFrame),
316
314
  output_is_scalar=idx_out.equals(IDX_SCALAR),
@@ -318,3 +316,5 @@ def transform_pandas(
318
316
  True if dim_out is None else isinstance(idx_out, MultiIndex)
319
317
  ),
320
318
  )
319
+
320
+ return result # type:ignore