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.
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/PKG-INFO +15 -6
- data_disaggregation-0.10.3/README.md +13 -0
- data_disaggregation-0.10.3/data_disaggregation/__init__.py +5 -0
- data_disaggregation-0.10.3/data_disaggregation/actions.py +7 -0
- data_disaggregation-0.10.3/data_disaggregation/base.py +185 -0
- data_disaggregation-0.10.3/data_disaggregation/ext.py +320 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation/utils.py +54 -34
- data_disaggregation-0.10.3/data_disaggregation/vtypes.py +88 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/PKG-INFO +15 -6
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/SOURCES.txt +2 -1
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/setup.py +1 -1
- data_disaggregation-0.10.3/test/test.py +518 -0
- data-disaggregation-0.9.3/README.md +0 -13
- data-disaggregation-0.9.3/data_disaggregation/__init__.py +0 -16
- data-disaggregation-0.9.3/data_disaggregation/base.py +0 -168
- data-disaggregation-0.9.3/data_disaggregation/classes.py +0 -76
- data-disaggregation-0.9.3/data_disaggregation/ext.py +0 -223
- data-disaggregation-0.9.3/test/test.py +0 -551
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/LICENSE +0 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/dependency_links.txt +0 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/requires.txt +0 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/data_disaggregation.egg-info/top_level.txt +0 -0
- {data-disaggregation-0.9.3 → data_disaggregation-0.10.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: data-disaggregation
|
|
3
|
-
Version: 0.
|
|
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
|
-
#
|
|
24
|
+
# README
|
|
16
25
|
|
|
17
|
-

|
|
18
|
-

|
|
26
|
+
[](./LICENSE)
|
|
27
|
+
[](https://github.com/wingechr/data-disaggregation)
|
|
19
28
|
[](https://pypi.org/project/data-disaggregation/)
|
|
20
|
-

|
|
29
|
+
[](https://github.com/wingechr/data-disaggregation/actions/workflows/test.yml)
|
|
21
30
|
[](https://wingechr.github.io/data-disaggregation/)
|
|
22
31
|
|
|
23
32
|
## Install
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# README
|
|
2
|
+
|
|
3
|
+
[](./LICENSE)
|
|
4
|
+
[](https://github.com/wingechr/data-disaggregation)
|
|
5
|
+
[](https://pypi.org/project/data-disaggregation/)
|
|
6
|
+
[](https://github.com/wingechr/data-disaggregation/actions/workflows/test.yml)
|
|
7
|
+
[](https://wingechr.github.io/data-disaggregation/)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install data-disaggregation
|
|
13
|
+
```
|
|
@@ -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
|
+
)
|