data-disaggregation 0.10.0__tar.gz → 0.11.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 (20) hide show
  1. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/PKG-INFO +15 -6
  2. data_disaggregation-0.11.0/README.md +13 -0
  3. data_disaggregation-0.11.0/data_disaggregation/__init__.py +5 -0
  4. data_disaggregation-0.11.0/data_disaggregation/actions.py +7 -0
  5. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation/base.py +61 -40
  6. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation/ext.py +79 -53
  7. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation/utils.py +48 -30
  8. data_disaggregation-0.11.0/data_disaggregation/vtypes.py +88 -0
  9. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation.egg-info/PKG-INFO +15 -6
  10. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation.egg-info/SOURCES.txt +2 -1
  11. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/setup.py +1 -1
  12. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/test/test.py +59 -44
  13. data-disaggregation-0.10.0/README.md +0 -13
  14. data-disaggregation-0.10.0/data_disaggregation/__init__.py +0 -15
  15. data-disaggregation-0.10.0/data_disaggregation/classes.py +0 -76
  16. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/LICENSE +0 -0
  17. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation.egg-info/dependency_links.txt +0 -0
  18. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation.egg-info/requires.txt +0 -0
  19. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/data_disaggregation.egg-info/top_level.txt +0 -0
  20. {data-disaggregation-0.10.0 → data_disaggregation-0.11.0}/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.10.0
3
+ Version: 0.11.0
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.11.0"
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"]
@@ -45,10 +45,8 @@ Helper to create the mapping
45
45
 
46
46
  """
47
47
 
48
-
49
48
  from typing import Mapping, Tuple
50
49
 
51
- from .classes import VT, F, T, V, VT_NumericExt
52
50
  from .utils import (
53
51
  as_set,
54
52
  group_idx_first,
@@ -60,42 +58,74 @@ from .utils import (
60
58
  is_unique,
61
59
  iter_values,
62
60
  )
61
+ from .vtypes import F, T, V, VariableType, VT_NumericExt
63
62
 
64
63
  VALIDATE_EQ_REL_TOLERANCE = 1e-10
65
64
 
66
65
 
67
66
  def transform(
68
- vtype: VT,
67
+ vtype: VariableType,
69
68
  data: Mapping[F, V],
70
69
  weight_map: Mapping[Tuple[F, T], float],
71
- size_in: Mapping[F, float] = None,
72
- size_out: Mapping[T, float] = None,
73
- threshold: float = 0.0,
74
- validate=True,
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,
75
74
  ) -> Mapping[T, V]:
76
- if size_in is None:
77
- size_in = group_idx_first(weight_map)
78
-
79
- if size_out is None:
80
- size_out = group_idx_second(weight_map)
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)
81
111
 
82
112
  if validate:
83
113
  # validate size_f
84
- assert is_mapping(size_in)
85
- assert is_unique(size_in)
86
- assert all(v > 0 for v in iter_values(size_in))
114
+ assert is_mapping(weights_from)
115
+ assert is_unique(weights_from)
116
+ assert all(v > 0 for v in iter_values(weights_from))
87
117
 
88
118
  # validate size_t
89
- assert is_mapping(size_out)
90
- assert is_unique(size_out)
91
- assert all(v > 0 for v in iter_values(size_out))
119
+ assert is_mapping(weights_to)
120
+ assert is_unique(weights_to)
121
+ assert all(v > 0 for v in iter_values(weights_to))
92
122
 
93
123
  # validate var
94
124
  assert is_mapping(data)
95
125
  assert is_unique(data)
96
126
 
97
- if not is_subset(data, size_in):
98
- err = as_set(data) - as_set(size_in)
127
+ if not is_subset(data, weights_from):
128
+ err = as_set(data) - as_set(weights_from)
99
129
  raise Exception(
100
130
  f"Variable index is not a subset of input dimension subset: {err}"
101
131
  )
@@ -104,8 +134,8 @@ def transform(
104
134
  assert is_map(weight_map)
105
135
  assert is_unique(weight_map)
106
136
  assert all(v >= 0 for v in iter_values(weight_map))
107
- assert is_subset([x[0] for x in weight_map.keys()], size_in)
108
- assert is_subset([x[1] for x in weight_map.keys()], size_out)
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)
109
139
  # assert all(isinstance(v, (float, int)) for v in iter_values(weight_map))
110
140
 
111
141
  # filter nan in data
@@ -113,22 +143,19 @@ def transform(
113
143
 
114
144
  # scale extensive => intensive
115
145
  if vtype == VT_NumericExt:
116
- try:
117
- data = dict((f, v / size_in[f]) for f, v in data.items())
118
- except TypeError:
119
- raise Exception(data)
146
+ data = dict((f, v / weights_from[f]) for f, v in data.items())
120
147
 
121
148
  # filter unused in weight_map: input:
122
149
  weight_map = dict(((f, t), w) for (f, t), w in weight_map.items() if f in data)
123
150
 
124
151
  # filter unused in weight_map: output
125
152
  weight_map = dict(
126
- ((f, t), w) for (f, t), w in weight_map.items() if size_out.get(t, 0) > 0
153
+ ((f, t), w) for (f, t), w in weight_map.items() if weights_to.get(t, 0) > 0
127
154
  )
128
155
 
129
156
  # init groups
130
157
  groups = dict((t, []) for t in set(_t for (_, _t) in weight_map.keys()))
131
- # group data
158
+ # group data by output keys
132
159
  for (f, t), w in weight_map.items():
133
160
  v = data[f]
134
161
  groups[t].append((v, w))
@@ -136,18 +163,12 @@ def transform(
136
163
  # create weight sums
137
164
  group_sumw = dict((t, sum(w for _, w in vws)) for t, vws in groups.items())
138
165
 
139
- if validate or threshold:
140
- sumw_rel = dict((t, sumw / size_out[t]) for t, sumw in group_sumw.items())
141
-
142
- # sumw always <= size_out ==> sumw_rel <= 1
143
- # TODO: maybe drop this check, this should never really happen
144
- # if validate:
145
- # scomp = 1 + VALIDATE_EQ_REL_TOLERANCE
146
- # assert all(s <= scomp for s in sumw_rel.values()), sumw_rel
147
-
148
166
  # drop groups under threshold
149
- if threshold:
150
- groups = dict((t, vws) for t, vws in groups.items() if sumw_rel[t] >= 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
+ )
151
172
 
152
173
  result = {}
153
174
  for t, vws in groups.items():
@@ -159,6 +180,6 @@ def transform(
159
180
  # re-scale intensive => extensive
160
181
  if vtype == VT_NumericExt:
161
182
  for t, v in result.items():
162
- result[t] = v * size_out[t]
183
+ result[t] = v * weights_to[t]
163
184
 
164
185
  return result
@@ -1,16 +1,17 @@
1
1
  """extended functions, especially for pandas Series
2
2
  """
3
+
3
4
  from typing import List, Tuple, Union
4
5
 
5
6
  import numpy as np
6
7
  from pandas import DataFrame, Index, MultiIndex, Series
7
8
 
8
9
  from .base import transform
9
- from .classes import SCALAR_DIM_NAME, SCALAR_INDEX_KEY, VT
10
10
  from .utils import is_scalar
11
+ from .vtypes import SCALAR_DIM_NAME, SCALAR_INDEX_KEY, VariableType
11
12
 
12
13
  IDX_SCALAR = MultiIndex.from_product([Index([SCALAR_INDEX_KEY], name=SCALAR_DIM_NAME)])
13
- COL_VAL = "__WEIGHT__"
14
+ COL_WEIGHT = "__WEIGHT__"
14
15
  COL_FROM = "__FROM__"
15
16
  COL_TO = "__TO__"
16
17
 
@@ -20,31 +21,29 @@ def harmonize_input_data(data: Union[DataFrame, Series, float]) -> DataFrame:
20
21
  return DataFrame with MultiIndex
21
22
  """
22
23
  if is_scalar(data):
23
- return DataFrame({SCALAR_INDEX_KEY: data}, index=IDX_SCALAR)
24
- if isinstance(data, Series):
24
+ data = DataFrame({SCALAR_INDEX_KEY: data}, index=IDX_SCALAR)
25
+ elif isinstance(data, Series):
25
26
  data = data.to_frame()
26
27
  # ensure multiindex
27
28
  data = ensure_multiindex(data)
28
29
  return data
29
30
 
30
31
 
31
- def as_multiindex(item: Index) -> MultiIndex:
32
- if not isinstance(item, MultiIndex):
33
- item = MultiIndex.from_product([item])
34
-
35
- assert isinstance(item, MultiIndex)
36
- return item
32
+ def as_multiindex(index: Index) -> MultiIndex:
33
+ if not isinstance(index, MultiIndex):
34
+ index = MultiIndex.from_product([index])
35
+ return index
37
36
 
38
37
 
39
38
  def ensure_multiindex(item: Union[DataFrame, Series]) -> Union[DataFrame, Series]:
40
39
  if not isinstance(item.index, MultiIndex):
41
- item = item.copy()
42
- item.index = as_multiindex(item.index)
43
- assert 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
44
43
  return item
45
44
 
46
45
 
47
- def as_list_of_series(
46
+ def as_list_of_series_w_multiindex(
48
47
  items: Union[Index, Series, Tuple[Union[Index, Series]]]
49
48
  ) -> List[Series]:
50
49
  # make sure we have a list/tuple
@@ -52,14 +51,13 @@ def as_list_of_series(
52
51
  items = [items]
53
52
  # make sure we have series:
54
53
  items = [it if isinstance(it, Series) else Series(1, index=it) for it in items]
55
- for it in items:
56
- valdidate_index(it)
54
+ items = [ensure_multiindex(it) for it in items]
57
55
  return items
58
56
 
59
57
 
60
58
  def merge_indices(items: List[Union[Series, Index]]) -> MultiIndex:
61
59
  """Create product of unions of indices"""
62
- # ensure items are indices
60
+ # ensure items are multiindices
63
61
  items = [it if isinstance(it, Index) else it.index for it in items]
64
62
  items = [as_multiindex(it) for it in items]
65
63
  indices = {}
@@ -83,16 +81,20 @@ def combine_weights(
83
81
  Series with MultiIndex
84
82
  """
85
83
  # make sure we have series:
86
- weights = as_list_of_series(weights)
84
+ weights = as_list_of_series_w_multiindex(weights)
85
+
87
86
  # merge indices
88
87
  idx = merge_indices(weights)
88
+
89
89
  # multiply all and drop nan
90
90
  result = Series(1, index=idx)
91
91
  for w in weights:
92
- # IMPORTANT: `result *= w` gives a different result,so DONT Use it
92
+ # IMPORTANT: `result *= w` gives a different result, so DONT use it
93
93
  result = result * w
94
+
94
95
  # multiplications implicit join can change order:
95
- result.index = result.index.reorder_levels(idx.names)
96
+ if isinstance(result.index, MultiIndex):
97
+ result.index = result.index.reorder_levels(idx.names)
96
98
 
97
99
  # drop nan
98
100
  result.dropna(inplace=True)
@@ -142,6 +144,7 @@ def remap_series_to_frame(s: Series, idx: MultiIndex, colname: str) -> DataFrame
142
144
  # convert index into columns
143
145
  df_result.reset_index(inplace=True)
144
146
  # set index to same levels as `s`, but keep columns
147
+
145
148
  df_result.set_index(s.index.names, drop=False, inplace=True)
146
149
  df_result = ensure_multiindex(df_result)
147
150
 
@@ -163,7 +166,7 @@ def create_weight_map(
163
166
  """
164
167
  idx_all = merge_indices([idx_in, idx_out])
165
168
  # expand index (TODO: check if weights are dropped??)
166
- df = remap_series_to_frame(ds_weights, idx_all, COL_VAL)
169
+ df = remap_series_to_frame(ds_weights, idx_all, COL_WEIGHT)
167
170
  df[COL_FROM] = list(zip(*[df[n] for n in idx_in.names]))
168
171
  df[COL_TO] = list(zip(*[df[n] for n in idx_out.names]))
169
172
 
@@ -171,34 +174,64 @@ def create_weight_map(
171
174
  df = df.loc[df[COL_FROM].isin(idx_in)]
172
175
  df = df.loc[df[COL_TO].isin(idx_out)]
173
176
 
174
- ds_weight_map = df.set_index([COL_FROM, COL_TO])[COL_VAL]
177
+ ds_weight_map = df.set_index([COL_FROM, COL_TO])[COL_WEIGHT]
175
178
  ds_weight_map = ds_weight_map.fillna(0)
176
179
 
177
180
  return ds_weight_map
178
181
 
179
182
 
180
- def valdidate_index(item: Union[Index, Series, DataFrame]):
183
+ def validate_multiindex(item: Union[Index, Series, DataFrame]):
181
184
  if isinstance(item, (Series, DataFrame)):
182
185
  item = item.index
183
-
184
- assert len(set(item.names)) == len(item.names)
185
- assert item.is_unique
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
186
190
 
187
191
 
188
192
  def transform_pandas(
189
- vtype: VT,
193
+ vtype: VariableType,
190
194
  data: Union[DataFrame, Series, float],
191
195
  weights: Union[Index, Series, Tuple[Union[Index, Series]]],
192
196
  dim_in: Union[Index, Series] = None,
193
197
  dim_out: Union[Index, Series] = None,
198
+ validate: bool = True,
194
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
+
195
224
  # ensure data is DataFrame with MultiIndex
196
225
  df_data = harmonize_input_data(data)
197
- valdidate_index(df_data)
226
+
227
+ if validate:
228
+ validate_multiindex(df_data)
198
229
 
199
230
  # combine weights into single Series with MultiIndex
200
231
  ds_weights = combine_weights(weights)
201
- valdidate_index(ds_weights)
232
+
233
+ if validate:
234
+ validate_multiindex(ds_weights)
202
235
 
203
236
  # determine input index
204
237
  if dim_in is None:
@@ -222,16 +255,17 @@ def transform_pandas(
222
255
  ds_size_out = ensure_multiindex(dim_out)
223
256
  idx_out = ds_size_out.index
224
257
 
225
- valdidate_index(idx_in)
226
- valdidate_index(idx_out)
258
+ if validate:
259
+ validate_multiindex(idx_in)
260
+ validate_multiindex(idx_out)
227
261
 
228
262
  # create weight map
229
263
  ds_weight_map = create_weight_map(ds_weights, idx_in, idx_out)
230
264
 
231
- valdidate_index(ds_weight_map)
232
-
233
265
  if ds_size_in is None:
234
- ds_size_in = ds_weight_map.reset_index().groupby(COL_FROM).sum(COL_VAL)[COL_VAL]
266
+ ds_size_in = (
267
+ ds_weight_map.reset_index().groupby(COL_FROM).sum(COL_WEIGHT)[COL_WEIGHT]
268
+ )
235
269
  # fix index
236
270
  ds_size_in.index = MultiIndex.from_tuples(
237
271
  ds_size_in.index.values, names=idx_in.names
@@ -239,28 +273,20 @@ def transform_pandas(
239
273
  ds_size_in = ds_size_in.loc[ds_size_in > 0]
240
274
 
241
275
  if ds_size_out is None:
242
- ds_size_out = ds_weight_map.reset_index().groupby(COL_TO).sum(COL_VAL)[COL_VAL]
276
+ ds_size_out = (
277
+ ds_weight_map.reset_index().groupby(COL_TO).sum(COL_WEIGHT)[COL_WEIGHT]
278
+ )
243
279
  # fix index
244
280
  ds_size_out.index = MultiIndex.from_tuples(
245
281
  ds_size_out.index.values, names=idx_out.names
246
282
  )
247
283
  ds_size_out = ds_size_out.loc[ds_size_out > 0]
248
284
 
249
- valdidate_index(ds_size_in)
250
- valdidate_index(ds_size_out)
251
-
252
- assert ds_size_in.index.isin(idx_in).all()
253
- assert ds_size_out.index.isin(idx_out).all()
254
-
255
- # TODO drop from ds_weight_map
256
- # TODO: drop from data
257
-
258
- # df_data = df_data[idx_in]
259
-
260
- # raise Exception(idx_in[idx_in.isin(ds_size_in.index)])
261
-
262
- # assert ds_size_in.index.equals(idx_in), (ds_size_in.index, idx_in)
263
- # assert ds_size_out.index.equals(idx_out), (ds_size_out.index, idx_out)
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()
264
290
 
265
291
  idx_in = ds_size_in.index
266
292
  idx_out = ds_size_out.index
@@ -275,9 +301,9 @@ def transform_pandas(
275
301
  vtype=vtype,
276
302
  data=s_col,
277
303
  weight_map=ds_weight_map,
278
- size_in=ds_size_in,
279
- size_out=ds_size_out,
280
- threshold=0.0,
304
+ weights_from=ds_size_in,
305
+ weights_to=ds_size_out,
306
+ weight_rel_threshold=0.0,
281
307
  validate=False, # we do validation in pandas
282
308
  )
283
309
 
@@ -1,22 +1,27 @@
1
1
  """utility functions
2
2
  """
3
+
3
4
  import math
4
5
  from typing import Callable, List, Mapping, Tuple
5
6
 
6
7
  from pandas import DataFrame, Index, Series
7
8
 
8
- from . import classes
9
+ from . import vtypes
9
10
 
10
11
 
11
12
  def group_sum(key_vals: Mapping, get_key: Callable = None) -> Mapping:
12
- """simple group sum
13
+ """simple group sum.
13
14
 
14
- Args:
15
- key_vals (list): non empty list of (key, value) pairs
16
- * keys can be anything hashable,
17
- * values must be numerical
15
+ Parameters
16
+ ----------
17
+ key_vals: Mapping
18
+ * keys can be anything hashable,
19
+ * values must be numerical
20
+ get_key: Callable = None
18
21
 
19
22
  Returns
23
+ -------
24
+ : Mapping
20
25
  list of (unique key, sum of values) pairs
21
26
  """
22
27
 
@@ -34,15 +39,19 @@ def group_sum(key_vals: Mapping, get_key: Callable = None) -> Mapping:
34
39
 
35
40
 
36
41
  def weighted_sum(value_normweights: Tuple[float]) -> float:
37
- """get sum product
42
+ """get sum product.
38
43
 
39
- Args:
40
- value_normweights (list): non empty list of (value, weight) pairs
41
- * values must be numerical
42
- * weights must be numerical, positive, and sum up to 1.0
44
+ Parameters
45
+ ----------
46
+ value_normweights : list
47
+ non empty list of (value, weight) pairs.
48
+ * values must be numerical.
49
+ * weights must be numerical, positive, and sum up to 1.0.
43
50
 
44
51
  Returns
45
- key from original list with highest weight
52
+ -------
53
+ : float
54
+
46
55
  """
47
56
  # TODO faster methods with numpy or ?
48
57
  return sum(v * w for v, w in value_normweights)
@@ -51,13 +60,16 @@ def weighted_sum(value_normweights: Tuple[float]) -> float:
51
60
  def weighted_mode(value_normweights: Tuple):
52
61
  """get most common value (but by weight)
53
62
 
54
- Args:
55
- value_normweights (list): non empty list of (value, weight) pairs
56
- * values can be anything sortable
57
- * weights must be numerical, positive, and sum up to 1.0
63
+ Parameters
64
+ ----------
65
+ value_normweights : list
66
+ non empty list of (value, weight) pairs.
67
+ * values must be anything sortable.
68
+ * weights must be numerical, positive, and sum up to 1.0.
58
69
 
59
70
  Returns
60
- key from original list with highest weight
71
+ -------
72
+ Any
61
73
  """
62
74
  # make values unique (sum weights)
63
75
  value_normweights = group_sum(value_normweights).items()
@@ -68,13 +80,16 @@ def weighted_mode(value_normweights: Tuple):
68
80
  def weighted_percentile(value_normweights: Tuple, p=0.5):
69
81
  """get most median (but by weight)
70
82
 
71
- Args:
72
- value_normweights (list): non empty list of (value, weight) pairs
73
- * values can be anything sortable
74
- * weights must be numerical, positive, and sum up to 1.0
83
+ Parameters
84
+ ----------
85
+ value_normweights : list
86
+ non empty list of (value, weight) pairs.
87
+ * values must be anything sortable.
88
+ * weights must be numerical, positive, and sum up to 1.0.
75
89
 
76
90
  Returns
77
- key from original list with highest weight
91
+ -------
92
+ Any
78
93
  """
79
94
  # make values unique (sum weights)
80
95
  value_normweights = group_sum(value_normweights).items()
@@ -90,13 +105,16 @@ def weighted_percentile(value_normweights: Tuple, p=0.5):
90
105
  def weighted_median(value_normweights: Tuple):
91
106
  """get most median (but by weight)
92
107
 
93
- Args:
94
- value_normweights (list): non empty list of (value, weight) pairs
95
- * values can be anything sortable
96
- * weights must be numerical, positive, and sum up to 1.0
108
+ Parameters
109
+ ----------
110
+ value_normweights : list
111
+ non empty list of (value, weight) pairs.
112
+ * values must be anything sortable.
113
+ * weights must be numerical, positive, and sum up to 1.0.
97
114
 
98
115
  Returns
99
- key from original list with highest weight
116
+ -------
117
+ Any
100
118
  """
101
119
  return weighted_percentile(value_normweights, p=0.5)
102
120
 
@@ -160,7 +178,7 @@ def as_mapping(x, default_val=1) -> Mapping:
160
178
  elif is_list(x):
161
179
  return dict((k, default_val) for k in x)
162
180
  elif is_scalar(x):
163
- return {classes.SCALAR_INDEX_KEY: x}
181
+ return {vtypes.SCALAR_INDEX_KEY: x}
164
182
  raise TypeError(x)
165
183
 
166
184
 
@@ -168,8 +186,8 @@ def as_scalar(x):
168
186
  if as_scalar(x):
169
187
  return x
170
188
  elif is_mapping(x):
171
- assert set(x.keys()) == set([classes.SCALAR_INDEX_KEY])
172
- return x[classes.SCALAR_INDEX_KEY]
189
+ assert set(x.keys()) == set([vtypes.SCALAR_INDEX_KEY])
190
+ return x[vtypes.SCALAR_INDEX_KEY]
173
191
  raise TypeError(x)
174
192
 
175
193
 
@@ -0,0 +1,88 @@
1
+ """Type classes for data.
2
+ """
3
+
4
+ from abc import ABC
5
+ from typing import TypeVar
6
+
7
+ from . import utils
8
+
9
+ F = TypeVar("F")
10
+ T = TypeVar("T")
11
+ V = TypeVar("V")
12
+
13
+ SCALAR_DIM_NAME = "__SCALAR__"
14
+ # TODO: using None in pandas causes problems with autoconvert to nan
15
+ SCALAR_INDEX_KEY = "__SCALAR__"
16
+
17
+
18
+ class VariableType(ABC):
19
+ @classmethod
20
+ def weighted_aggregate(cls, data):
21
+ """aggregate data
22
+
23
+ Parameters
24
+ ----------
25
+ data : Iterable
26
+ non empty list of (value, weight) pairs.
27
+ weights must be numerical, positive, and sum up to 1.0.
28
+
29
+ Returns
30
+ -------
31
+ aggregated value
32
+ """
33
+ raise NotImplementedError()
34
+
35
+
36
+ class VT_Nominal(VariableType):
37
+ """Type class for nominal (categorical) data.
38
+
39
+ - Aggregation method: mode (most commonly used)
40
+ - Disaggregation method: keep value
41
+ - Examples: regional codes
42
+ """
43
+
44
+ @classmethod
45
+ def weighted_aggregate(cls, data):
46
+ return utils.weighted_mode(data)
47
+
48
+
49
+ class VT_Ordinal(VT_Nominal):
50
+ """Type class for ordinal data (ranked categorical).
51
+
52
+ - Aggregation method: median
53
+ - Disaggregation method: keep value
54
+ - Examples: Level of agreement
55
+ """
56
+
57
+ @classmethod
58
+ def weighted_aggregate(cls, data):
59
+ return utils.weighted_median(data)
60
+
61
+
62
+ class VT_Numeric(VariableType):
63
+ """Type class for numerical, intensive data
64
+
65
+ An intensive variable is one which does not scale with the system size.
66
+
67
+ - Aggregation method: weighted average
68
+ - Disaggregation method: keep value
69
+ - Examples: temperature, density, pressure
70
+ """
71
+
72
+ @classmethod
73
+ def weighted_aggregate(cls, data):
74
+ return utils.weighted_sum(data)
75
+
76
+
77
+ class VT_NumericExt(VT_Numeric):
78
+ """Type class for numerical, extensive data.
79
+
80
+ An extensive variable is one which does scale with the system size
81
+ (assuming an equal distribution).
82
+
83
+ - Aggregation method: sum
84
+ - Disaggregation method: distribute by weights
85
+ - Examples: population, energy, total cost
86
+ """
87
+
88
+ pass
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: data-disaggregation
3
- Version: 0.10.0
3
+ Version: 0.11.0
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
@@ -2,10 +2,11 @@ LICENSE
2
2
  README.md
3
3
  setup.py
4
4
  data_disaggregation/__init__.py
5
+ data_disaggregation/actions.py
5
6
  data_disaggregation/base.py
6
- data_disaggregation/classes.py
7
7
  data_disaggregation/ext.py
8
8
  data_disaggregation/utils.py
9
+ data_disaggregation/vtypes.py
9
10
  data_disaggregation.egg-info/PKG-INFO
10
11
  data_disaggregation.egg-info/SOURCES.txt
11
12
  data_disaggregation.egg-info/dependency_links.txt
@@ -12,7 +12,7 @@ if __name__ == "__main__":
12
12
  description="",
13
13
  long_description=long_description_md,
14
14
  long_description_content_type="text/markdown",
15
- version="0.10.0",
15
+ version="0.11.0",
16
16
  author="Christian Winger",
17
17
  platforms=["any"],
18
18
  license="MIT",
@@ -1,3 +1,4 @@
1
+ import doctest
1
2
  import logging
2
3
  from functools import partial
3
4
  from unittest import TestCase
@@ -6,18 +7,13 @@ import numpy as np
6
7
  import pandas as pd
7
8
  from pandas import DataFrame, Index, MultiIndex, Series
8
9
 
10
+ from data_disaggregation import ext
9
11
  from data_disaggregation.base import transform
10
- from data_disaggregation.classes import (
11
- SCALAR_INDEX_KEY,
12
- VT_Nominal,
13
- VT_Numeric,
14
- VT_NumericExt,
15
- VT_Ordinal,
16
- )
17
12
  from data_disaggregation.ext import (
18
13
  COL_FROM,
19
14
  COL_TO,
20
- COL_VAL,
15
+ COL_WEIGHT,
16
+ as_multiindex,
21
17
  combine_weights,
22
18
  create_weight_map,
23
19
  ensure_multiindex,
@@ -37,6 +33,13 @@ from data_disaggregation.utils import (
37
33
  weighted_mode,
38
34
  weighted_sum,
39
35
  )
36
+ from data_disaggregation.vtypes import (
37
+ SCALAR_INDEX_KEY,
38
+ VT_Nominal,
39
+ VT_Numeric,
40
+ VT_NumericExt,
41
+ VT_Ordinal,
42
+ )
40
43
 
41
44
  logging.basicConfig(
42
45
  format="[%(asctime)s %(levelname)7s] %(message)s",
@@ -78,7 +81,6 @@ class TestUtils(TestCase):
78
81
  (float("inf"), True),
79
82
  (float("nan"), True),
80
83
  (np.nan, True),
81
- (np.NAN, True),
82
84
  ]:
83
85
  self.assertEqual(is_na(x), y)
84
86
 
@@ -124,8 +126,6 @@ class TestUtils(TestCase):
124
126
 
125
127
 
126
128
  class TestBase(TestCase):
127
- """"""
128
-
129
129
  def get_example(self, vtype):
130
130
  """
131
131
  M | D E F | S | V
@@ -255,7 +255,7 @@ class TestBaseExamples(TestCase):
255
255
  (2, SCALAR_INDEX_KEY): 20,
256
256
  (3, SCALAR_INDEX_KEY): 30,
257
257
  },
258
- threshold=0.5,
258
+ weight_rel_threshold=0.5,
259
259
  )
260
260
  self.assertAlmostEqual(res[SCALAR_INDEX_KEY], 1 + 1 + 10)
261
261
 
@@ -285,9 +285,9 @@ class TestBaseExamples(TestCase):
285
285
  (SCALAR_INDEX_KEY, "11"): 0.99, # cell will be dropped (size = 2)
286
286
  (SCALAR_INDEX_KEY, "10"): 1.1, # (size = 2)
287
287
  },
288
- size_in={SCALAR_INDEX_KEY: 5},
289
- size_out={"00": 1, "01": 1, "11": 2, "10": 2},
290
- threshold=0.5,
288
+ weights_from={SCALAR_INDEX_KEY: 5},
289
+ weights_to={"00": 1, "01": 1, "11": 2, "10": 2},
290
+ weight_rel_threshold=0.5,
291
291
  )
292
292
 
293
293
  self.assertAlmostEqual(res["00"], 100 / 5)
@@ -337,6 +337,13 @@ class TestBaseExamples(TestCase):
337
337
 
338
338
  class TextExtPandas(TestCase):
339
339
  def assertPandasEqal(self, left, right):
340
+ if isinstance(left, Index):
341
+ left = left.sort_values()
342
+ right = right.sort_values()
343
+ else:
344
+ left = left.sort_index()
345
+ right = right.sort_index()
346
+
340
347
  if isinstance(left, Index):
341
348
  method = pd.testing.assert_index_equal
342
349
  elif isinstance(left, Series):
@@ -398,20 +405,19 @@ class TextExtPandas(TestCase):
398
405
 
399
406
  def test_combine_weights_1(self):
400
407
  # no overlap
401
- s_res = Series(
402
- [2, 3, 2, 3],
403
- index=MultiIndex.from_tuples(
404
- [(11, 21), (11, 22), (12, 21), (12, 22)], names=["i1", "i2"]
405
- ),
406
- )
407
408
  self.assertPandasEqal(
408
- s_res,
409
409
  combine_weights(
410
410
  [
411
411
  Index([11, 12], name="i1"),
412
412
  Series([2, 3], index=Index([21, 22], name="i2")),
413
413
  ]
414
414
  ),
415
+ Series(
416
+ [2, 3, 2, 3],
417
+ index=MultiIndex.from_tuples(
418
+ [(11, 21), (11, 22), (12, 21), (12, 22)], names=["i1", "i2"]
419
+ ),
420
+ ),
415
421
  )
416
422
 
417
423
  def test_combine_weights_2(self):
@@ -459,7 +465,7 @@ class TextExtPandas(TestCase):
459
465
  ],
460
466
  names=[COL_FROM, COL_TO],
461
467
  ),
462
- name=COL_VAL,
468
+ name=COL_WEIGHT,
463
469
  )
464
470
 
465
471
  self.assertPandasEqal(ds_result, create_weight_map(ds_weights, idx_in, idx_out))
@@ -477,27 +483,36 @@ class TextExtPandas(TestCase):
477
483
  ensure_multiindex(DataFrame({"v": [1, 1]}, index=Index([1, 1], name="i1"))),
478
484
  )
479
485
 
480
- def test_transform_pandas_1(self):
481
- exp_res = Series(
482
- [1 / (2 + 1), 2 / (2 + 1), 2.0],
483
- index=MultiIndex.from_tuples([("a",), ("b",), ("c",)], names=["i2"]),
484
- name="s1",
485
- )
486
- res = transform_pandas(
487
- vtype=VT_NumericExt,
488
- data=Series([1, 2], index=Index([0, 1], name="i1"), name="s1"),
489
- weights=Series(
490
- [1, 2, 1.5],
491
- index=MultiIndex.from_tuples(
492
- [(0, "a"), (0, "b"), (1, "c")], names=["i1", "i2"]
486
+ def test_pandas_utils(self):
487
+ for idx in [Index(["b", "a"], name="d1")]:
488
+ idx2 = as_multiindex(idx)
489
+ self.assertEqual(idx.names, idx2.names)
490
+ self.assertEqual(tuple(tuple(x) for x in idx.values), tuple(idx2.values))
491
+
492
+ def test_transform_pandas(self):
493
+ self.assertPandasEqal(
494
+ transform_pandas(
495
+ vtype=VT_NumericExt,
496
+ data=Series([1, 2], index=Index([0, 1], name="i1"), name="s1"),
497
+ weights=Series(
498
+ [1, 2, 1.5],
499
+ index=MultiIndex.from_tuples(
500
+ [(0, "a"), (0, "b"), (1, "c")], names=["i1", "i2"]
501
+ ),
493
502
  ),
494
503
  ),
504
+ Series(
505
+ [1 / (2 + 1), 2 / (2 + 1), 2.0],
506
+ index=MultiIndex.from_tuples([("a",), ("b",), ("c",)], names=["i2"]),
507
+ name="s1",
508
+ ),
495
509
  )
496
- self.assertPandasEqal(
497
- exp_res,
498
- res,
499
- )
500
- self.assertPandasEqal(
501
- exp_res.to_frame("V"),
502
- res.to_frame("V"),
503
- )
510
+
511
+
512
+ class Doctests(TestCase):
513
+ def run_doctest(self, module):
514
+ report = doctest.testmod(module)
515
+ self.assertFalse(report.failed)
516
+
517
+ def test_modules(self):
518
+ self.run_doctest(ext)
@@ -1,13 +0,0 @@
1
- # DataDisAggregation
2
-
3
- ![](https://img.shields.io/github/license/wingechr/data-disaggregation)
4
- ![](https://img.shields.io/pypi/pyversions/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)
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
- ```
@@ -1,15 +0,0 @@
1
- __version__ = "0.10.0"
2
-
3
- # isort: skip_file -> keep order to prevent circular import
4
- from .ext import transform_pandas
5
- from .base import transform
6
- from .classes import VT_Nominal, VT_Numeric, VT_NumericExt, VT_Ordinal
7
-
8
- __all__ = [
9
- "VT_Nominal",
10
- "VT_Ordinal",
11
- "VT_Numeric",
12
- "VT_NumericExt",
13
- "transform_pandas",
14
- "transform",
15
- ]
@@ -1,76 +0,0 @@
1
- """classes and types
2
- """
3
-
4
- from abc import ABC
5
- from typing import TypeVar
6
-
7
- from . import utils
8
-
9
- F = TypeVar("F")
10
- T = TypeVar("T")
11
- V = TypeVar("V")
12
-
13
- SCALAR_DIM_NAME = ""
14
- # TODO: using None in pandas causes problems with autoconvert to nan
15
- SCALAR_INDEX_KEY = ""
16
-
17
-
18
- class VT(ABC):
19
- @classmethod
20
- def weighted_aggregate(cls, data):
21
- """aggregation
22
-
23
- Args:
24
- data (list): non empty list of (value, weight) pairs
25
-
26
- Returns
27
- aggregated value
28
- """
29
- raise NotImplementedError()
30
-
31
-
32
- class VT_Nominal(VT):
33
- """
34
- Examples: Regional Codes
35
- """
36
-
37
- @classmethod
38
- def weighted_aggregate(cls, data):
39
- return utils.weighted_mode(data)
40
-
41
-
42
- class VT_Ordinal(VT_Nominal):
43
- """
44
- Values can be sorted in a meaningful way
45
- Usually, that means using numerical codes that
46
- do not represent a metric distance, liek a likert scale
47
-
48
- Examples: [1 = "a little", 2 = "somewhat", 3 = "a lot"]
49
-
50
- """
51
-
52
- @classmethod
53
- def weighted_aggregate(cls, data):
54
- return utils.weighted_median(data)
55
-
56
-
57
- class VT_Numeric(VT):
58
- """
59
- * Values can be calculated by linear combinations
60
- * Examples: height, temperature, density
61
- """
62
-
63
- @classmethod
64
- def weighted_aggregate(cls, data):
65
- return utils.weighted_sum(data)
66
-
67
-
68
- class VT_NumericExt(VT_Numeric):
69
- """
70
- Values are extensive, i.e. they are can be transformed into intensive
71
- by dividing by domain size
72
-
73
- * Examples: population, energy production
74
- """
75
-
76
- pass