From 0c748aa3bd23d82a94602f87ca563c28fc754710 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 11:54:10 +0100 Subject: [PATCH 001/733] change to the meta lookup for descriptive view names --- core/view.py | 16 ++++++++++++---- core/view_generators/view_maps.py | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/view.py b/core/view.py index 3e29732ee..c70058846 100644 --- a/core/view.py +++ b/core/view.py @@ -141,11 +141,19 @@ def spec_relation(self, link=None): def _descriptives_relation(self, link): try: - if '[{' in link.x: - set_name = link.x.split('[{')[0] + link.x.split('}]')[-1] - x_values = [int(x['value']) for x in link.get_meta()['lib']['values'][set_name]] + # if '[{' in link.x: + # set_name = link.x.split('[{')[0] + link.x.split('}]')[-1] + # x_values = [int(x['value']) for x in link.get_meta()['lib']['values'][set_name]] + # else: + # x_values = [int(x['value']) for x in link.get_meta()['columns'][link.x]['values']] + values = link.get_meta()['columns'][link.x].get('values', None) + if 'lib@values' in values: + vals = values.split('@')[-1] + values = link.get_meta()['lib']['values'][vals] + x_values = [int(x['value']) for x in values] else: - x_values = [int(x['value']) for x in link.get_meta()['columns'][link.x]['values']] + x_values = [int(x['value']) for x in + link.get_meta()['columns'][link.x]['values']] if self.missing(): x_values = [x for x in x_values if not x in self.missing()] if self.rescaling(): diff --git a/core/view_generators/view_maps.py b/core/view_generators/view_maps.py index e31a3b75a..ac55acc08 100644 --- a/core/view_generators/view_maps.py +++ b/core/view_generators/view_maps.py @@ -383,7 +383,11 @@ def coltests(self, link, name, kwargs): view.dataframe = view_df view.name = notation link[notation] = view - except: + except Exception, e: + print '*'*60 + print link.x, link.y + print in_view + print e pass From 990141fa07d4d15d3bf4428d47aa83618178c4b7 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 14:01:00 +0100 Subject: [PATCH 002/733] change to empty sig test production: check for self.invalid --- core/quantify/engine.py | 2 +- core/view_generators/view_maps.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 8dc90b62d..2c4cc142f 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -1347,7 +1347,7 @@ def _empty_output(self): """ values = self.values values[:] = np.NaN - if values.shape == (1, 1) or values.shape == (1, 0): + if values.shape == (1, 1) or values.shape == (1, 0) or self.invalid: values = [np.NaN] return pd.DataFrame(values, index=self.multiindex[0], diff --git a/core/view_generators/view_maps.py b/core/view_generators/view_maps.py index ac55acc08..e31a3b75a 100644 --- a/core/view_generators/view_maps.py +++ b/core/view_generators/view_maps.py @@ -383,11 +383,7 @@ def coltests(self, link, name, kwargs): view.dataframe = view_df view.name = notation link[notation] = view - except Exception, e: - print '*'*60 - print link.x, link.y - print in_view - print e + except: pass From dc9bde1f03413fdfc2ed51d4c4adaff209281cdd Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 14:31:15 +0100 Subject: [PATCH 003/733] cleanup. --- core/view.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/view.py b/core/view.py index c70058846..e8ef5cbc4 100644 --- a/core/view.py +++ b/core/view.py @@ -141,11 +141,6 @@ def spec_relation(self, link=None): def _descriptives_relation(self, link): try: - # if '[{' in link.x: - # set_name = link.x.split('[{')[0] + link.x.split('}]')[-1] - # x_values = [int(x['value']) for x in link.get_meta()['lib']['values'][set_name]] - # else: - # x_values = [int(x['value']) for x in link.get_meta()['columns'][link.x]['values']] values = link.get_meta()['columns'][link.x].get('values', None) if 'lib@values' in values: vals = values.split('@')[-1] From 8eb731d0c226d0391190e30c22a3d9de31e2f9a9 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 15:22:17 +0100 Subject: [PATCH 004/733] bugfix for catching empty counts correctly. --- core/quantify/engine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 2c4cc142f..8d95a7cfa 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -1346,8 +1346,11 @@ def _empty_output(self): """ """ values = self.values + print values + print self.multiindex values[:] = np.NaN - if values.shape == (1, 1) or values.shape == (1, 0) or self.invalid: + + if values.shape == (1, 1) or values.shape == (1, 0) or (self.invalid and self.metric == 'means'): values = [np.NaN] return pd.DataFrame(values, index=self.multiindex[0], From 04204c31c00eefa4f23abd3167fd0c830e057fb1 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 15:27:18 +0100 Subject: [PATCH 005/733] print statements removed --- core/quantify/engine.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 8d95a7cfa..175694660 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -1346,10 +1346,7 @@ def _empty_output(self): """ """ values = self.values - print values - print self.multiindex values[:] = np.NaN - if values.shape == (1, 1) or values.shape == (1, 0) or (self.invalid and self.metric == 'means'): values = [np.NaN] return pd.DataFrame(values, From 6ee7489203bba0c121cb364ce4d53f7b6a8b777b Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 30 Oct 2015 16:02:06 +0100 Subject: [PATCH 006/733] another fix to route empty sigs. --- core/quantify/engine.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 175694660..6b7127e11 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -924,6 +924,8 @@ def __init__(self, link, view_name_notation): else: self.metric = 'proportions' self.invalid = None + self.no_pairs = None + self.no_diffs = None self.parameters = None self.mimic = None self.level = None @@ -999,12 +1001,18 @@ def set_params(self, level='mid', mimic='Dim', testtype='pooled', # Check if the aggregation is non-empty # and that there are >1 populated columns if np.nansum(self.values) == 0 or len(self.ydef) == 1: - self.invalid = True + self.invalid = True + if np.nansum(self.values) == 0: + self.no_diffs = True + if len(self.ydef) == 1: + self.no_pairs == True self.mimic = mimic self.comparevalue, self.level = self._convert_level(level) else: # Set global test algorithm parameters self.invalid = False + self.no_diffs = False + self.no_pairs = False # Deactived for now, access to user-defined test setup will be # made availabe at later stage! # valid_types = ['pooled', 'unpooled'] @@ -1345,10 +1353,28 @@ def _output(self, sigs): def _empty_output(self): """ """ + values = self.values - values[:] = np.NaN - if values.shape == (1, 1) or values.shape == (1, 0) or (self.invalid and self.metric == 'means'): - values = [np.NaN] + + if self.metric == 'proportions': + if self.no_pairs or self.no_diffs: + values[:] = np.NaN + if values.shape == (1, 1) or values.shape == (1, 0): + values = [np.NaN] + if self.metric == 'means': + if self.no_pairs or self.no_diffs: + values = [np.NaN] + + # if self.no_diffs: + # values[:] = np.NaN + # if self.no_pairs: + # values = [np.NaN] + + + # values = self.values + # values[:] = np.NaN + # if values.shape == (1, 1) or values.shape == (1, 0) or (self.invalid and self.metric == 'means'): + # values = [np.NaN] return pd.DataFrame(values, index=self.multiindex[0], columns=self.multiindex[1]) \ No newline at end of file From 036954afe69b0b848e02152e8a9ce19a63c0e830 Mon Sep 17 00:00:00 2001 From: James Griffiths Date: Fri, 30 Oct 2015 12:14:33 -0400 Subject: [PATCH 007/733] Correct typo. --- core/quantify/engine.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 6b7127e11..6c69e0e92 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -1005,7 +1005,7 @@ def set_params(self, level='mid', mimic='Dim', testtype='pooled', if np.nansum(self.values) == 0: self.no_diffs = True if len(self.ydef) == 1: - self.no_pairs == True + self.no_pairs = True self.mimic = mimic self.comparevalue, self.level = self._convert_level(level) else: @@ -1355,7 +1355,6 @@ def _empty_output(self): """ values = self.values - if self.metric == 'proportions': if self.no_pairs or self.no_diffs: values[:] = np.NaN From 318cfb54d1784bd62114a661b45714e398682511 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Fri, 20 Nov 2015 15:49:29 +0000 Subject: [PATCH 008/733] Raise exception for when a cluster is empty --- core/builds/powerpoint/pptx_painter.py | 4 ++++ core/builds/powerpoint/transformations.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index 97f1ecb9d..ab7d90c70 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -184,7 +184,11 @@ def PowerPointPainter(path_pptx, pptx_start_time = time.time() + if not cluster: + raise Exception("'{}' cluster is empty".format(cluster_name)) + validate_cluster_orientations(cluster) + orientation = cluster[cluster.keys()[0]].orientation print('\nPowerPoint minions are building your PPTX, ' diff --git a/core/builds/powerpoint/transformations.py b/core/builds/powerpoint/transformations.py index 08390e147..3732c99fe 100644 --- a/core/builds/powerpoint/transformations.py +++ b/core/builds/powerpoint/transformations.py @@ -39,7 +39,11 @@ def case_insensitive_matcher(check_these, against_this): matched items from the df. ''' - matched = [v for x,d in enumerate(check_these) for i,v in enumerate(against_this) if v.lower() == d.lower()] + matched = [v + for x,d in enumerate(check_these) + for i,v in enumerate(against_this) + if v.lower() == d.lower() + ] return matched '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' @@ -298,7 +302,7 @@ def place_vals_in_labels(old_df, base_position=0, orientation='side', drop_posit return new_df - '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' +'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def get_qestion_labels(cluster_name, meta, table_name=None): From a4bfb0f9836c3922869422e9527b960c07e446c6 Mon Sep 17 00:00:00 2001 From: James Griffiths Date: Fri, 20 Nov 2015 12:19:38 -0500 Subject: [PATCH 009/733] Added dummy rules_weight arg to stack.get_chain() --- core/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/stack.py b/core/stack.py index 0f30f5920..de154e601 100644 --- a/core/stack.py +++ b/core/stack.py @@ -259,7 +259,7 @@ def variable_types(self, data_key, only_type=None): def get_chain(self, name=None, data_keys=None, filters=None, x=None, y=None, views=None, post_process=True, orient_on=None, select=None, - rules=False): + rules=False, rules_weight=None): """ Construct a "chain" shaped subset of Links and their Views from the Stack. From 1e54552a39c9566508bf2d8e3e2d7d19b3278427 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Mon, 23 Nov 2015 12:35:00 +0000 Subject: [PATCH 010/733] update --- core/builds/powerpoint/pptx_painter.py | 32 ++++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index e71e4efa8..2815905fd 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -510,20 +510,27 @@ def PowerPointPainter(path_pptx, if not weighted_chart: if not view.is_weighted(): - # weighted col % + # unweighted col % if not view.is_net(): + ''' ignore questions if they are copied from another question ''' + if not copied_from: + ''' exclude fixed categories while sorting ''' + if sort_order == 'ascending': + vdf = sort_df(vdf, + fixed_categories, + column_position=0, + ascend=True) + elif sort_order == 'descending': + vdf = sort_df(vdf, + fixed_categories, + column_position=0, + ascend=False) + df = paint_df(vdf, view, meta, text_key) - # format question labels to grid index labels - grid_element_label = strip_html_tags(df.index[0][0]) - if ' - ' in grid_element_label: - grid_element_label = grid_element_label.split(' - ')[-1].strip() - if '. ' in grid_element_label: - grid_element_label = grid_element_label.split('. ',1)[-1].strip() - df = partition_view_df(df)[0] views_on_var.append(df) - # weighted net + # unweighted net elif view.is_net(): if include_nets: original_labels = vdf.index.tolist() @@ -693,13 +700,8 @@ def PowerPointPainter(path_pptx, # Y ORIENTATION CODE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ############################################################################ -<<<<<<< HEAD elif orientation == 'y': -======= - if orientation == 'y': - ->>>>>>> i228-Uneven_grid_element_len raise TypeError('y orientation not supported yet') @@ -710,4 +712,4 @@ def PowerPointPainter(path_pptx, time=pptx_elapsed_time, line= '_' * 80 ) - ) \ No newline at end of file + ) From 1b750aa96dc55f005fcabf70d1c29c19d5dcd56e Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Mon, 23 Nov 2015 14:24:33 +0000 Subject: [PATCH 011/733] update2 --- core/builds/powerpoint/pptx_painter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index 2815905fd..a7d0c8e7b 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -701,7 +701,6 @@ def PowerPointPainter(path_pptx, ############################################################################ elif orientation == 'y': - raise TypeError('y orientation not supported yet') From 974beab394902012bc4ebcc702a577b824d71416 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Thu, 10 Dec 2015 11:11:00 +0000 Subject: [PATCH 012/733] fix net view painting --- core/builds/powerpoint/pptx_painter.py | 27 ++------------------------ 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index a8854c6a1..63f44d3bc 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -221,7 +221,7 @@ def PowerPointPainter(path_pptx, if crossbreak == '@': '----BUILD DATAFRAME---------------------------------------------' - # decide whether to use weight or unweight c% data for charts + # decide whether to use weighted or unweighted c% data for charts weighted_chart = [el for el in chain.views if el.startswith('x|frequency|') and el.split('|')[4]!='' and el.split('|')[3]=='y'] @@ -259,13 +259,7 @@ def PowerPointPainter(path_pptx, # weighted net elif view.is_net(): if include_nets: - original_labels = vdf.index.tolist() df = paint_df(vdf, view, meta, text_key) - df_labels = df.index.tolist() - new_idx = (df_labels[0][0], original_labels[0][1]) - df.index = pd.MultiIndex.from_tuples([new_idx], - names=['Question', 'Values']) - df = partition_view_df(df)[0] views_on_var.append(df) @@ -287,13 +281,7 @@ def PowerPointPainter(path_pptx, # unweighted net elif view.is_net(): if include_nets: - original_labels = vdf.index.tolist() df = paint_df(vdf, view, meta, text_key) - df_labels = df.index.tolist() - new_idx = (df_labels[0][0], original_labels[0][1]) - df.index = pd.MultiIndex.from_tuples([new_idx], - names=['Question', 'Values']) - df = partition_view_df(df)[0] views_on_var.append(df) @@ -506,13 +494,7 @@ def PowerPointPainter(path_pptx, # weighted net elif view.is_net(): if include_nets: - original_labels = vdf.index.tolist() df = paint_df(vdf, view, meta, text_key) - df_labels = df.index.tolist() - new_idx = (df_labels[0][0], original_labels[0][1]) - df.index = pd.MultiIndex.from_tuples([new_idx], - names=['Question', 'Values']) - df = partition_view_df(df)[0] views_on_var.append(df) @@ -541,13 +523,7 @@ def PowerPointPainter(path_pptx, # unweighted net elif view.is_net(): if include_nets: - original_labels = vdf.index.tolist() df = paint_df(vdf, view, meta, text_key) - df_labels = df.index.tolist() - new_idx = (df_labels[0][0], original_labels[0][1]) - df.index = pd.MultiIndex.from_tuples([new_idx], - names=['Question', 'Values']) - df = partition_view_df(df)[0] views_on_var.append(df) @@ -709,6 +685,7 @@ def PowerPointPainter(path_pptx, prs.save('{}.pptx'.format(path_pptx)) if orientation == 'y': + raise TypeError('y orientation not supported yet') pptx_elapsed_time = time.time() - pptx_start_time From e28b472fc8427b38fe50908b6430118d9731e278 Mon Sep 17 00:00:00 2001 From: Alasdair Eaglestone Date: Thu, 14 Jan 2016 11:02:00 +0000 Subject: [PATCH 013/733] np.inf/ -np.inf now identified, in ExcelPainters paint_box() function, and written as '-'. --- core/builds/excel/excel_painter.py | 549 ++++++++++++++--------------- 1 file changed, 272 insertions(+), 277 deletions(-) diff --git a/core/builds/excel/excel_painter.py b/core/builds/excel/excel_painter.py index 73491c181..25c5120c3 100644 --- a/core/builds/excel/excel_painter.py +++ b/core/builds/excel/excel_painter.py @@ -225,32 +225,27 @@ def paint_box(worksheet, frames, format_dict, rows, cols, metas, formats_spec, data = frames[idxf].head( box_coord[0] // len(frames)+1 - ).values[-1][box_coord[1]] - - # post-process cell data + ).values[-1][box_coord[1]] - # ebase - convert numpy.inf - if shortname == 'ebase': - if data == np.inf: - data = str(np.inf) + # post-process cell data # % - divide data by 100 for formatting in Excel - elif rel_to in ['x', 'y'] and not method in ['coltests', + if rel_to in ['x', 'y'] and not method in ['coltests', 'descriptives']: data = data / 100 # coltests - convert NaN to '', otherwise get column letters - elif method == 'coltests': + elif method == 'coltests': if pd.isnull(data) or data == 0: - data = '' - else: - x = data.replace('[', '').replace(']', '') - if len(x) == 1: - data = testcol_map[x] - else: - data = '' + data = '' + else: + x = data.replace('[', '').replace(']', '') + if len(x) == 1: + data = testcol_map[x] + else: + data = '' for letter in x.split(', '): - data += testcol_map[letter] + formats_spec.test_seperator + data += testcol_map[letter] + formats_spec.test_seperator data = data[:-len(formats_spec.test_seperator)] # replace 0 with char @@ -267,7 +262,7 @@ def paint_box(worksheet, frames, format_dict, rows, cols, metas, formats_spec, # Check data for NaN and replace with '-' if not isinstance(data, (str, unicode)): - if np.isnan(data): + if np.isnan(data) or np.isinf(data): data = '-' # Italicise? @@ -281,9 +276,9 @@ def paint_box(worksheet, frames, format_dict, rows, cols, metas, formats_spec, # write data try: worksheet.write( - coord[0], - coord[1], - data, + coord[0], + coord[1], + data, format_dict[format_name] ) except Exception, e: @@ -293,7 +288,7 @@ def paint_box(worksheet, frames, format_dict, rows, cols, metas, formats_spec, '{0:<15}{1:<15}{2:<30}{3:<30}{4}'.format( 'DATA', 'CELL', 'FORMAT', 'VIEW FULLNAME', 'ERROR' ), - '{0:<15}{1:<15}{2:<30}{3:<30}{4}'.format( + '{0:<15}{1:<15}{2:<30}{3:<30}{4}'.format( data, xl_rowcol_to_cell(coord[0], coord[1]), format_name, @@ -301,10 +296,10 @@ def paint_box(worksheet, frames, format_dict, rows, cols, metas, formats_spec, e )] ) - ) - + ) + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' -def set_row_height(worksheet, +def set_row_height(worksheet, row_start, row_stop, row_height, @@ -366,11 +361,11 @@ def write_column_labels(worksheet, labels, existing_format, row, ) for i, col in enumerate(xrange(cols[0], cols[1]+1)): worksheet.merge_range( - row+1, - col, - row+1+(levels*2), - col, - labels[1][i], + row+1, + col, + row+1+(levels*2), + col, + labels[1][i], existing_format ) elif len(labels) > 2: @@ -391,11 +386,11 @@ def write_column_labels(worksheet, labels, existing_format, row, for x in xrange(X): # write header(s) worksheet.merge_range( - row+(lev*2), - cols[0]+(N*x), - row+(lev*2), - cols[0]+(N*(x+1))-1, - labels[(lev*2)][0], + row+(lev*2), + cols[0]+(N*x), + row+(lev*2), + cols[0]+(N*(x+1))-1, + labels[(lev*2)][0], existing_format ) @@ -403,41 +398,41 @@ def write_column_labels(worksheet, labels, existing_format, row, if n > 1: for col in xrange(len(labels[(lev*2)+1])): worksheet.merge_range( - row+(lev*2)+1, - cols[0]+(N*x)+(n*col), - row+(lev*2)+1, - cols[0]+(N*x)+(n*col)+(n-1), - labels[(lev*2)+1][col], + row+(lev*2)+1, + cols[0]+(N*x)+(n*col), + row+(lev*2)+1, + cols[0]+(N*x)+(n*col)+(n-1), + labels[(lev*2)+1][col], existing_format ) else: if R == 0: worksheet.write_row( - row+(lev*2)+1, - cols[0], + row+(lev*2)+1, + cols[0], labels[(lev*2)+1]*( (cols[1]-cols[0]+1)/len(labels[-1]) - ), + ), existing_format ) else: for col in xrange(len(labels[(lev*2)+1])): worksheet.merge_range( - row+(lev*2)+1, - cols[0]+(N*x)+(n*col), - row+(lev*2)+R+1, - cols[0]+(N*x)+(n*col), + row+(lev*2)+1, + cols[0]+(N*x)+(n*col), + row+(lev*2)+R+1, + cols[0]+(N*x)+(n*col), labels[(lev*2)+1][col], existing_format ) except: pass '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' -def write_category_labels(worksheet, +def write_category_labels(worksheet, labels, existing_format, row, - col, + col, row_height=None, row_wrap_trigger=None, group_size=1, @@ -466,37 +461,37 @@ def write_category_labels(worksheet, if lab_len < row_wrap_trigger: if group_size > 1 and set_heights: set_row_height( - worksheet=worksheet, - row_start=row+(idx*group_size), + worksheet=worksheet, + row_start=row+(idx*group_size), row_stop=row+(idx*group_size)+(group_size-1), row_height=row_height ) else: set_row_height( - worksheet=worksheet, - row_start=row+(idx*group_size), + worksheet=worksheet, + row_start=row+(idx*group_size), row_stop=row+(idx*group_size), row_height=row_height ) elif group_size > 1 and set_heights: set_row_height( - worksheet=worksheet, - row_start=row+(idx*group_size)+1, + worksheet=worksheet, + row_start=row+(idx*group_size)+1, row_stop=row+(idx*group_size)+(group_size-1), row_height=row_height ) if isinstance(lab, float): worksheet.write_number( - row+(idx*group_size), - col, - lab, + row+(idx*group_size), + col, + lab, existing_format ) else: worksheet.write( - row+(idx*group_size), - col, - lab, + row+(idx*group_size), + col, + lab, existing_format ) except: @@ -555,7 +550,7 @@ def view_generator(chain_views, grouped_views=[], ordered=False): else: if all(isinstance(item, str) for item in grouped_views): non_grouped_views = [ - view for view in chain_views + view for view in chain_views if view not in grouped_views ] for view in non_grouped_views: @@ -565,10 +560,10 @@ def view_generator(chain_views, grouped_views=[], ordered=False): elif any(view in chain_views for view in grouped_views): for view in grouped_views: if view in chain_views: - yield [view] + yield [view] elif all(isinstance(item, list) for item in grouped_views): chained_grouped_views = list(itertools.chain(*grouped_views)) - non_grouped_views = [x for x in chain_views + non_grouped_views = [x for x in chain_views if not x in chained_grouped_views] for view in non_grouped_views: yield [view] @@ -607,9 +602,9 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): idx_last = 0 len_last = 0 dummy_rows = 0 - + bumped_views = [] - + x_iter = { 'y': xy_generator(chain), 'x': [chain.source_name] @@ -617,7 +612,7 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): view_sizes = chain.view_sizes() view_lengths = chain.view_lengths() - + for xy in x_iter[chain.orientation]: group_order = grouped_views[:] try: @@ -626,8 +621,8 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): idxs = 0 if xy not in offset_dict.keys(): offset_dict[xy] = OrderedDict() - for view in view_generator(chain.views): - if not view[0] in offset_dict[xy].keys(): + for view in view_generator(chain.views): + if not view[0] in offset_dict[xy].keys(): if view[0] in grouped_views: idxv = chain.views.index( group_order.pop(group_order.index(view[0])) @@ -661,7 +656,7 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): offset_dict[xy][bv] = temp_a + temp_b bumped_views = [] elif len(bumped_views) > 0: - for bv in bumped_views: + for bv in bumped_views: pbv = next(reversed(offset_dict[xy])) temp_a = offset_dict[xy][pbv] pbv_index = chain.views.index(pbv) @@ -673,7 +668,7 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): offset_dict[xy][k] += dummy_rows if not key_last.endswith('cbase') and len(key_last) > 0: cond_1 = not key_last.split('|')[1].startswith('tests.') - cond_2 = not view[0].split('|')[1].startswith('tests.') + cond_2 = not view[0].split('|')[1].startswith('tests.') if cond_1 and cond_2: if not k in list(itertools.chain(*grouped_views)): offset_dict[xy][k] += (len_last) @@ -683,7 +678,7 @@ def get_view_offset(chain, offset_dict, grouped_views=[], dummy_tests=False): if k in group: break cond_1 = group.index(k) == 0 cond_2 = not any( - v.split('|')[1].startswith('tests.') + v.split('|')[1].startswith('tests.') for v in group ) if cond_1 or cond_2: @@ -724,7 +719,7 @@ def validate_cluster_orientations(cluster): - All chains must have the same orientation, x or y. ''' if len(set([ - cluster[chain_name].orientation + cluster[chain_name].orientation for chain_name in cluster.keys() ])) != 1: raise Exception( @@ -765,7 +760,7 @@ def ExcelPainter(path_excel, italicise_level=None, create_toc=False): """ - Builds excel file (XLSX) from cluster, list of clusters, or + Builds excel file (XLSX) from cluster, list of clusters, or dictionary of clusters. Parameters @@ -783,9 +778,9 @@ def ExcelPainter(path_excel, annotations : dict keys = cluster names, values = list of annotations for cells A1, A2, A3 display_names : list - list of axes to append question numbers to labels + list of axes to append question numbers to labels transform_names : dict - keys as x/ y key names, values as names to display, if using + keys as x/ y key names, values as names to display, if using display_names arg table_properties : dict keys as format properties, values as change from default @@ -805,7 +800,7 @@ def ExcelPainter(path_excel, "Either the value passed to 'grouped_views' or its structure is not" " valid. Please check it and again. The correct form is:" " {'name': [[vk1, vk2], [vk3, vk4], ...]}") - + if grouped_views is None: grouped_views = {} @@ -827,14 +822,14 @@ def ExcelPainter(path_excel, formats_spec.create_formats_dict() formats = { - key: workbook.add_format(formats_spec.format_dict[key]) + key: workbook.add_format(formats_spec.format_dict[key]) for key in formats_spec.format_dict.keys() } # Set starting row and column row_index_origin = formats_spec.start_row_idx+1 col_index_origin = formats_spec.start_column_idx-1 - + # Check the starting row/ column are not under the minimum # else apply the minimum if row_index_origin < 3: row_index_origin = 3 @@ -859,48 +854,48 @@ def ExcelPainter(path_excel, #create table of contents sheet toc_locs = [] toc_names = [] - toc_labels = [] - + toc_labels = [] + #transform banked chain specs to banked chains for cluster in clusters: for chain_name in cluster.keys(): if cluster[chain_name].get('type')=='banked-chain': cluster[chain_name] = cluster.bank_chains( - cluster[chain_name], + cluster[chain_name], text_key) - + if create_toc: - + TOCsheet = workbook.add_worksheet('TOC') TOCsheet.write(2, 1, 'Table of Contents', formats['TOC-bold-14']) TOCsheet.set_column(0, 0, 1) - + if isinstance(create_toc, bool): sheet_idx = [i for i in xrange(len(clusters))] elif isinstance(create_toc, list): - sheet_idx = [i for i, cl in enumerate(clusters) + sheet_idx = [i for i, cl in enumerate(clusters) if cl.name in create_toc] else: raise Exception('create_toc arg must be of type bool/ list') - + for idx in sheet_idx: TOCsheet.set_column( - 1+sheet_idx.index(idx), - 1+sheet_idx.index(idx), + 1+sheet_idx.index(idx), + 1+sheet_idx.index(idx), 10 ) TOCsheet.write( - 5, - 1+sheet_idx.index(idx), - names[idx], + 5, + 1+sheet_idx.index(idx), + names[idx], formats['TOC-bold-10'] ) TOCsheet.set_column(len(sheet_idx)+1, len(sheet_idx)+1, 1) TOCsheet.set_column(len(sheet_idx)+2, len(sheet_idx)+2, 125) TOCsheet.write( - 5, - len(sheet_idx)+2, - 'Question Text', + 5, + len(sheet_idx)+2, + 'Question Text', formats['TOC-bold-center-10'] ) TOCsheet.freeze_panes(6,0) @@ -920,55 +915,55 @@ def ExcelPainter(path_excel, toc_locs.append([]) toc_names.append([]) toc_labels.append([]) - + # add worksheet worksheet = workbook.add_worksheet(sheet_name) #need a better way to identify "profile" tables... if all([ - isinstance(item, pd.DataFrame) + isinstance(item, pd.DataFrame) for item in cluster.itervalues() - ]): - + ]): + worksheet.set_row(4, formats_spec.y_header_height) worksheet.set_row(5, formats_spec.y_row_height) - - for chain in chain_generator(cluster): - + + for chain in chain_generator(cluster): + # chain_format = chain.fillna('__NA__') chain_format = chain - + for column in chain_format.columns.tolist(): - + frames = [] vmetas = [] df_rows = [] df_cols = [] - + worksheet.set_column(0, 0, 40) - - series = chain_format[column] - + + series = chain_format[column] + if meta['columns'][column]['type'] in ['single']: categories = { - item['value']: item['text'][meta['lib']['default text']] + item['value']: item['text'][meta['lib']['default text']] for item in meta['columns'][column]['values'] - } + } series = series.map(categories.get, na_action='ignore') series = series.fillna('__NA__') elif meta['columns'][column]['type'] in ['delimited set']: categories = { - str(item['value']): item['text'][meta['lib']['default text']] + str(item['value']): item['text'][meta['lib']['default text']] for item in meta['columns'][column]['values'] } series = series.str.split(';').apply( pd.Series, 1 ).stack(dropna=False) - series = series.map(categories.get, + series = series.map(categories.get, na_action='ignore').unstack() # series.fillna('') series[series.columns[0]] = series[series.columns[0]].str.cat( - [series[c] for c in series.columns[1:]], + [series[c] for c in series.columns[1:]], sep=', ', na_rep='' ).str.slice(0, -2) @@ -981,42 +976,42 @@ def ExcelPainter(path_excel, else: series = series.fillna('__NA__') series = series.apply(unicoder) - + frames.append(series) df_rows.append((7, 7+frames[-1].shape[0])) - + colmax = int( - 0 - if worksheet.dim_colmax is None + 0 + if worksheet.dim_colmax is None else worksheet.dim_colmax ) df_cols.append((1+colmax, 1+colmax)) - - worksheet.set_column(df_cols[-1][0], - df_cols[-1][1], + + worksheet.set_column(df_cols[-1][0], + df_cols[-1][1], formats_spec.column_width_str) - + try: tk = meta['lib']['default text'] column_text = '. '.join( - [column, + [column, meta['columns'][column]['text'][tk]]) meta['columns'][column]['text'][tk] - worksheet.merge_range(4, df_cols[-1][0], + worksheet.merge_range(4, df_cols[-1][0], 5, df_cols[-1][0], column_text, formats['y']) except: - worksheet.merge_range(4, df_cols[-1][0], + worksheet.merge_range(4, df_cols[-1][0], 5, df_cols[-1][0], column, formats['y']) - + paint_box( - worksheet=worksheet, - frames=frames, - format_dict=formats, - rows=df_rows, - cols=df_cols, + worksheet=worksheet, + frames=frames, + format_dict=formats, + rows=df_rows, + cols=df_cols, metas=vmetas, formats_spec=formats_spec, ceil=True, @@ -1024,27 +1019,27 @@ def ExcelPainter(path_excel, ) worksheet.freeze_panes(6, 0) - + else: - + #validate_cluster validate_cluster_orientations(cluster) - + #nesting sizes nest_levels = get_nest_levels(cluster) - + #initialise row and col indices current_position = { 'x': row_index_origin+(nest_levels*2), 'y': col_index_origin, 'test': col_index_origin+1 } - + #update row index if freqs/ means tests? idxtestcol = 0 testcol_maps = {} for chain in chain_generator(cluster): - + view_sizes = chain.view_sizes() view_keys = chain.describe()['view'].values.tolist() has_props_tests = any([ @@ -1079,7 +1074,7 @@ def ExcelPainter(path_excel, values = helpers.emulate_meta(meta, values) y_values = [int(v) for v in zip(*[c for c in df.columns])[1]] values = [ - [value for value in values if value['value']==v][0] + [value for value in values if value['value']==v][0] for v in y_values ] for i in xrange(view_sizes[idxc][0][1]): @@ -1092,16 +1087,16 @@ def ExcelPainter(path_excel, testcol_labels = testcol_maps.keys() current_position['x'] += bool(testcol_maps) - + #dynamic coordinate map coordmap = { 'x': {}, 'y': {} } - + #offset dict offset = OrderedDict() - + #column & headings size set_row_height = True @@ -1112,14 +1107,14 @@ def ExcelPainter(path_excel, view_sizes = chain.view_sizes() view_lengths = chain.view_lengths() - + if chain.orientation=='x' and not chain.annotations is None: len_chain_annotations = len(chain.annotations) if len_chain_annotations > 0: for ann in chain.annotations: worksheet.write( - current_position['x']-1, - col_index_origin-1, + current_position['x']-1, + col_index_origin-1, helpers.get_text( ann, text_key, @@ -1131,7 +1126,7 @@ def ExcelPainter(path_excel, len_chain_annotations = 0 orientation = chain.orientation - + #chain's view offset if not offset: current_views = [] @@ -1149,9 +1144,9 @@ def ExcelPainter(path_excel, new_views = set(offset[offset.keys()[0]].keys()) \ - set(current_views) - + if chain.source_name not in coordmap[orientation].keys(): - + if orientation == 'y': coordmap['y'][chain.source_name] = [ current_position['y'], @@ -1172,15 +1167,15 @@ def ExcelPainter(path_excel, ] for xy in xy_generator(chain): - + if orientation == 'y': x, y = xy, chain.source_name elif orientation == 'x': y, x = xy, chain.source_name - + idxs = chain.content_of_axis.index(xy) - - #fill xs' ceil_floor + + #fill xs' ceil_floor ceiling, _ = min(offset[x].iteritems(), key=lambda o: o[1]) floor, _ = max(offset[x].iteritems(), key=lambda o: o[1]) @@ -1211,7 +1206,7 @@ def ExcelPainter(path_excel, ] elif orientation == 'x': if y not in coordmap['y'].keys(): - idxs = chain.content_of_axis.index(y) + idxs = chain.content_of_axis.index(y) coordmap['y'][y] = [ current_position['y'], current_position['y'] \ @@ -1221,17 +1216,17 @@ def ExcelPainter(path_excel, if dummy_tests: dummy_row_count = 0 - #loop views + #loop views for views in view_generator(offset[x].keys(), cluster_gv): - + frames = [] vmetas = [] vlevels = [] df_rows = [] df_cols = [] - + for idx, v in enumerate(views): - + view = chain[chain.data_key][chain.filter][x][y][v] if not isinstance(view, qp.View): @@ -1249,8 +1244,8 @@ def ExcelPainter(path_excel, ) ) - if all(view.meta()['agg'][key] == value - for key, value in [('name', 'cbase'), + if all(view.meta()['agg'][key] == value + for key, value in [('name', 'cbase'), ('is_weighted', False)]): a = view.dataframe.values[0] for cbindex, cb in np.ndenumerate(a): @@ -1276,7 +1271,7 @@ def ExcelPainter(path_excel, vlevels.append(view.is_meanstest()) else: vlevels.append(None) - + if view.meta()['agg']['method'] == 'frequency': agg_name = view.meta()['agg']['name'] if agg_name in ['cbase', 'c%', 'r%', 'counts']: @@ -1284,7 +1279,7 @@ def ExcelPainter(path_excel, if chain.is_banked: axes.remove('x') df = helpers.paint_dataframe( - meta=meta, + meta=meta, df=view.dataframe.copy(), text_key=text_key, display_names=display_names, @@ -1295,7 +1290,7 @@ def ExcelPainter(path_excel, df = view.dataframe.copy() else: df = view.dataframe.copy() - + #write column test labels if 'test' in view.meta()['agg']['method']: if view.meta()['y']['name'] in testcol_labels: @@ -1307,31 +1302,31 @@ def ExcelPainter(path_excel, ) for i, code in enumerate(y_values): worksheet.write( - row_index_origin+(nest_levels*2)-1, + row_index_origin+(nest_levels*2)-1, current_position['test']+i, - testcol_maps[view.meta()['y']['name']][str(code)], + testcol_maps[view.meta()['y']['name']][str(code)], formats['tests'] ) current_position['test'] += view.meta()['shape'][1] testcol_labels.remove( view.meta()['y']['name'] ) - + #append frame to frames frames.append(df) - + #get dataframe and it's coordinates df_rows.append( coordmap['x'][x][view.meta()['agg']['fullname']] ) df_cols.append(coordmap['y'][y]) - + # Add dummy dfs if dummy_tests: cond_1 = len(frames) == 1 cond_2 = ( len(frames) > 1 and not any( - vm['agg']['method'] == 'coltests' + vm['agg']['method'] == 'coltests' for vm in vmetas ) ) @@ -1348,47 +1343,47 @@ def ExcelPainter(path_excel, df_rows[0][0]+len_rows]) df_cols.append(coordmap['y'][y]) dummy_row_count += len_rows - + #write data is_ceil = vmetas[0]['agg']['fullname'] == ceiling is_floor = vmetas[-1]['agg']['fullname'] == floor - - # has weighted views - sub_chain = chain[chain.data_key][chain.filter] + + # has weighted views + sub_chain = chain[chain.data_key][chain.filter] has_weighted_views = any( sub_chain[xk][yk][vk].meta()['agg']['is_weighted'] - for xk in sub_chain.keys() - for yk in sub_chain[xk].keys() + for xk in sub_chain.keys() + for yk in sub_chain[xk].keys() for vk in sub_chain[xk][yk].keys() - ) + ) if view.meta()['y']['name'] in testcol_maps: paint_box( - worksheet=worksheet, - frames=frames, - format_dict=formats, - rows=df_rows, - cols=df_cols, - metas=vmetas, + worksheet=worksheet, + frames=frames, + format_dict=formats, + rows=df_rows, + cols=df_cols, + metas=vmetas, formats_spec=formats_spec, has_weighted_views=has_weighted_views, y_italicise=y_italicise, - ceil=is_ceil, - floor=is_floor, + ceil=is_ceil, + floor=is_floor, testcol_map=testcol_maps[view.meta()['y']['name']] ) else: paint_box( - worksheet=worksheet, - frames=frames, - format_dict=formats, - rows=df_rows, - cols=df_cols, - metas=vmetas, + worksheet=worksheet, + frames=frames, + format_dict=formats, + rows=df_rows, + cols=df_cols, + metas=vmetas, formats_spec=formats_spec, has_weighted_views=has_weighted_views, y_italicise=y_italicise, - ceil=is_ceil, + ceil=is_ceil, floor=is_floor ) @@ -1403,31 +1398,31 @@ def ExcelPainter(path_excel, vmetas[idx]['agg']['is_weighted'] ) relation = fullname.split('|')[2] - + #write y labels - NESTING WORKING FOR 2 LEVELS. NEEDS TO WORK FOR N LEVELS. y_name = 'Total' if y_name == '@' else y_name - + if y_name == 'Total': if coordmap['x'][x_name][fullname][0] == row_index_origin+(nest_levels*2) + bool(testcol_maps) + len_chain_annotations: #write column label(s) - multi-column y subaxis worksheet.set_column( - df_cols[idx][0], - df_cols[idx][1], + df_cols[idx][0], + df_cols[idx][1], 10 ) worksheet.merge_range( - row_index_origin-3, - df_cols[idx][0], - row_index_origin+(nest_levels*2)+bool(testcol_maps)+len_chain_annotations-2, - df_cols[idx][1], - y_name, + row_index_origin-3, + df_cols[idx][0], + row_index_origin+(nest_levels*2)+bool(testcol_maps)+len_chain_annotations-2, + df_cols[idx][1], + y_name, formats['y'] ) if bool(testcol_maps): worksheet.write( - row_index_origin+(nest_levels*2)-1, - 1, - '', + row_index_origin+(nest_levels*2)-1, + 1, + '', formats['tests'] ) else: @@ -1435,35 +1430,35 @@ def ExcelPainter(path_excel, labels = helpers.get_unique_level_values(df.columns) if nest_levels == 0: write_column_labels( - worksheet, - labels, + worksheet, + labels, formats['y'], - row_index_origin-3, + row_index_origin-3, df_cols[idx] ) elif nest_levels > 0: - write_column_labels(worksheet, - labels, - formats['y'], - row_index_origin-3, - df_cols[idx], + write_column_labels(worksheet, + labels, + formats['y'], + row_index_origin-3, + df_cols[idx], nest_levels ) - + #write x labels if df_cols[0][0] == col_index_origin: if fullname == ceiling: - + write_question_label( - worksheet, - df.index[0][0], - formats['x_left_bold'], + worksheet, + df.index[0][0], + formats['x_left_bold'], df_rows[idx][0]-1, col_index_origin-1, formats_spec.row_height, formats_spec.row_wrap_trigger ) - + if create_toc: toc_locs[-1].append( (df_rows[idx][0]-1, col_index_origin-1) @@ -1472,38 +1467,38 @@ def ExcelPainter(path_excel, toc_names[-1].append( transform_names.get(x_name, x_name)) - else: + else: toc_names[-1].append(x_name) - if 'x' in display_names: + if 'x' in display_names: toc_label_parts = df.index[0][0].split( '. ') if len(toc_label_parts) == 0: toc_label = toc_label_parts[0] else: toc_label = ''.join( - toc_label_parts[1:]) + toc_label_parts[1:]) toc_labels[-1].append(toc_label) else: - toc_labels[-1].append(df.index[0][0]) + toc_labels[-1].append(df.index[0][0]) cond_1 = df_cols[0][0] == col_index_origin cond_2 = fullname in new_views - if cond_1 or cond_2: + if cond_1 or cond_2: if shortname == 'cbase': if has_weighted_views and not is_weighted: if len(text) > 0: format_key = 'x_right_ubase' - labels = [''.join(['Unweighted ', + labels = [''.join(['Unweighted ', text.lower()])] else: format_key = 'x_right_base' labels = [fullname] write_category_labels( - worksheet=worksheet, - labels=labels, - existing_format=formats[format_key], - row=df_rows[idx][0], - col=col_index_origin-1, + worksheet=worksheet, + labels=labels, + existing_format=formats[format_key], + row=df_rows[idx][0], + col=col_index_origin-1, row_height=formats_spec.row_height, row_wrap_trigger=formats_spec.row_wrap_trigger, set_heights=True @@ -1514,7 +1509,7 @@ def ExcelPainter(path_excel, text = '{}: {}'.format( text, helpers.get_text( - unicoder(chain.base_text, + unicoder(chain.base_text, like_ascii=True), text_key, 'x')) @@ -1523,17 +1518,17 @@ def ExcelPainter(path_excel, labels = [fullname] format_key = 'x_right_base' write_category_labels( - worksheet=worksheet, - labels=labels, - existing_format=formats[format_key], - row=df_rows[idx][0], - col=col_index_origin-1, + worksheet=worksheet, + labels=labels, + existing_format=formats[format_key], + row=df_rows[idx][0], + col=col_index_origin-1, row_height=formats_spec.row_height, row_wrap_trigger=formats_spec.row_wrap_trigger, set_heights=True ) - else: - if (vmetas[0]['agg']['method'] in ['descriptives'] or + else: + if (vmetas[0]['agg']['method'] in ['descriptives'] or (vmetas[0]['agg']['method'] in ['frequency'] and len(relation) > 0)): if len(frames) > 1: labels = [] @@ -1555,14 +1550,14 @@ def ExcelPainter(path_excel, labels = df.index.get_level_values(1) if all([label not in labels_written for label in labels]): write_category_labels( - worksheet=worksheet, - labels=labels, - existing_format=formats[format_key], - row=df_rows[0][0]+idxdf, - col=col_index_origin-1, + worksheet=worksheet, + labels=labels, + existing_format=formats[format_key], + row=df_rows[0][0]+idxdf, + col=col_index_origin-1, row_height=formats_spec.row_height, row_wrap_trigger=formats_spec.row_wrap_trigger, - group_size=len(frames), + group_size=len(frames), set_heights=True ) labels_written.extend(labels) @@ -1573,20 +1568,20 @@ def ExcelPainter(path_excel, format_key = 'x_right_nets' if len(frames[0].index) == 1: if len(vmetas[0]['agg']['text']) > 0: - labels = [vmetas[0]['agg']['text']] + labels = [vmetas[0]['agg']['text']] else: labels = df.index.get_level_values(1) else: - labels = df.index.get_level_values(1) + labels = df.index.get_level_values(1) write_category_labels( - worksheet=worksheet, - labels=labels, - existing_format=formats[format_key], - row=df_rows[0][0], - col=col_index_origin-1, + worksheet=worksheet, + labels=labels, + existing_format=formats[format_key], + row=df_rows[0][0], + col=col_index_origin-1, row_height=formats_spec.row_height, row_wrap_trigger=formats_spec.row_wrap_trigger, - group_size=len(frames), + group_size=len(frames), set_heights=True ) else: @@ -1609,17 +1604,17 @@ def ExcelPainter(path_excel, else: continue write_category_labels( - worksheet=worksheet, - labels=labels, - existing_format=formats[format_key], - row=df_rows[0][0]+idxdf, - col=col_index_origin-1, + worksheet=worksheet, + labels=labels, + existing_format=formats[format_key], + row=df_rows[0][0]+idxdf, + col=col_index_origin-1, row_height=formats_spec.row_height, row_wrap_trigger=formats_spec.row_wrap_trigger, group_size=len(frames), set_heights=True ) - + #increment row (only first occurrence of each x) if orientation == 'y': current_position['x'] += sum( @@ -1629,30 +1624,30 @@ def ExcelPainter(path_excel, current_position['y'] += ( coordmap['y'][xy][1]-coordmap['y'][xy][0]+1 ) - + #increment col if orientation == 'y': current_position['y'] += chain.source_length - + elif orientation == 'x': current_position['x'] += sum(view_lengths[0])+1 - if dummy_tests: + if dummy_tests: current_position['x'] += dummy_row_count #set column widths worksheet.set_column(col_index_origin-1, col_index_origin-1, 40) - + #set y axis height worksheet.set_row(row_index_origin-3, formats_spec.y_header_height) - worksheet.set_row(row_index_origin-2, formats_spec.y_row_height) - + worksheet.set_row(row_index_origin-2, formats_spec.y_row_height) + #freeze panes worksheet.freeze_panes( - row_index_origin+(nest_levels*2)+bool(testcol_maps)-1, + row_index_origin+(nest_levels*2)+bool(testcol_maps)-1, col_index_origin+1 ) - + #download image # if IMG_URL: if formats_spec.img_url and not formats_spec.no_logo: @@ -1682,7 +1677,7 @@ def ExcelPainter(path_excel, #post-process non-TOC sheets for worksheet in workbook.worksheets_objs: - + #hide gridlines worksheet.hide_gridlines(2) @@ -1698,10 +1693,10 @@ def ExcelPainter(path_excel, annotation = annotation_spec[0] annotation_format = workbook.add_format( annotation_spec[1] - ) + ) worksheet.write( - annotations[worksheet.name].index(annotation_spec), - 0, + annotations[worksheet.name].index(annotation_spec), + 0, annotation, annotation_format ) @@ -1718,33 +1713,33 @@ def ExcelPainter(path_excel, except: pass - #finish writing TOC + #finish writing TOC write_labels = all(name_list == toc_names[0] for name_list in toc_names) for i in xrange(len(toc_names)): for q in xrange(len(toc_names[i])): TOCsheet.write( 6+q, - 1+i, + 1+i, 'internal:%s!%s' % ( - names[i], + names[i], xl_rowcol_to_cell(toc_locs[i][q][0], toc_locs[i][q][1]) ), formats['TOC-url'] - ) + ) TOCsheet.write( - 6+q, - 1+i, - toc_names[i][q], + 6+q, + 1+i, + toc_names[i][q], formats['TOC-url'] - ) + ) if write_labels: if i == len(sheet_idx)-1: TOCsheet.write( - 6+q, - 3+i, - toc_labels[i][q], + 6+q, + 3+i, + toc_labels[i][q], formats['TOC-10'] ) - + #close excel file workbook.close() From e3dfa3e40488defa4702b6c612fb1261013c3d4d Mon Sep 17 00:00:00 2001 From: Alasdair Eaglestone Date: Fri, 19 Feb 2016 18:15:52 +0000 Subject: [PATCH 014/733] Conflict resolution --- core/builds/excel/excel_painter.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/builds/excel/excel_painter.py b/core/builds/excel/excel_painter.py index 992385efe..5d1064d10 100644 --- a/core/builds/excel/excel_painter.py +++ b/core/builds/excel/excel_painter.py @@ -1646,14 +1646,6 @@ def ExcelPainter(path_excel, transform_names=transform_names, axes=axes) elif view.meta()['agg']['is_block'] and not view.meta()['agg']['name'].startswith('NPS'): -<<<<<<< HEAD - format_block = view.meta()['agg']['is_block'] - block_ref = view.describe_block() - idx_order = get_ordered_index(view.dataframe.index) - block_ref_formats = [ - block_formats[block_ref[idxo]] - for idxo in idx_order] -======= if not is_net_only: format_block = view.meta()['agg']['is_block'] block_ref = view.describe_block() @@ -1665,7 +1657,6 @@ def ExcelPainter(path_excel, for idxo in idx_order) if brf_all_net: block_ref_formats = ['x_right_nets']*len(block_ref_formats) ->>>>>>> i389-excelpainter_gtm_updates df = helpers.paint_view( meta=meta, view=view, From 42749f46fa9adc5ba238cf26582b294cc2208ec4 Mon Sep 17 00:00:00 2001 From: James Griffiths Date: Mon, 7 Mar 2016 16:35:34 -0500 Subject: [PATCH 015/733] Added new optional parameters stack.save(..., dataset=False, describe=False). --- core/stack.py | 28 ++++++++++++++++++++++++++-- tests/test_stack.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/core/stack.py b/core/stack.py index 3112fe465..c691efc60 100644 --- a/core/stack.py +++ b/core/stack.py @@ -13,7 +13,7 @@ from view_generators.view_mapper import ViewMapper from view_generators.view_maps import QuantipyViews from quantipy.core.tools.dp.spss.reader import parse_sav_file -from quantipy.core.tools.dp.io import unicoder +from quantipy.core.tools.dp.io import unicoder, write_quantipy from quantipy.core.tools.dp.prep import frequency, verify_test_results from cache import Cache @@ -823,7 +823,7 @@ def refresh(self, data_key, new_data_key='', new_weight=None, return None def save(self, path_stack, compression="gzip", store_cache=True, - decode_str=False): + decode_str=False, dataset=False, describe=False): """ Save Stack instance to .stack file. @@ -839,6 +839,12 @@ def save(self, path_stack, compression="gzip", store_cache=True, decode_str : bool, default=True If True the unicoder function will be used to decode all str objects found anywhere in the meta document/s. + dataset : bool, default=False + If True a json/csv will be saved parallel to the saved stack + for each data key in the stack. + describe : bool, default=False + If True the result of stack.describe().to_excel() will be + saved parallel to the saved stack. Returns ------- @@ -884,6 +890,24 @@ def save(self, path_stack, compression="gzip", store_cache=True, f.close() + if dataset: + for key in self.keys(): + path_json = path_stack.replace( + '.stack', + ' [{}].json'.format(key)) + path_csv = path_stack.replace( + '.stack', + ' [{}].csv'.format(key)) + write_quantipy( + meta=self[key].meta, + data=self[key].data, + path_json=path_json, + path_csv=path_csv) + + if describe: + path_describe = path_stack.replace('.stack', '.xlsx') + self.describe().to_excel(path_describe) + # def get_slice(data_key=None, x=None, y=None, filters=None, views=None): # """ """ # pass diff --git a/tests/test_stack.py b/tests/test_stack.py index d447b253e..1a4ede788 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1066,6 +1066,43 @@ def test_save_load_stack_improved(self): if os.path.exists(path_stack): os.remove(path_stack) + def test_save_dataset(self): + # This tests save/load methods using the dataset + # parameter. + + path_stack = '%s%s.stack' % (self.path, self.stack.name) + self.setup_stack_Example_Data_A() + + self.stack.save(path_stack=path_stack, dataset=True) + + for key in self.stack.keys(): + path_json = path_stack.replace( + '.stack', + ' [{}].json'.format(key)) + path_csv = path_stack.replace( + '.stack', + ' [{}].csv'.format(key)) + self.assertTrue(os.path.exists(path_json)) + self.assertTrue(os.path.exists(path_csv)) + + os.remove(path_json) + os.remove(path_csv) + + + def test_save_describe(self): + # This tests save/load methods using the describe + # parameter. + + path_stack = '%s%s.stack' % (self.path, self.stack.name) + self.setup_stack_Example_Data_A() + + self.stack.save(path_stack=path_stack, describe=True) + + path_describe = path_stack.replace('.stack', '.xlsx') + self.assertTrue(os.path.exists(path_describe)) + os.remove(path_describe) + + @classmethod def tearDownClass(self): self.stack = Stack("StackName") From 8a891748d2dcb60963302b9456f66fe79d3a9f28 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Tue, 2 Aug 2016 15:11:03 +0100 Subject: [PATCH 016/733] try/except removed --- __init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 5093eb605..202f23b0a 100644 --- a/__init__.py +++ b/__init__.py @@ -24,10 +24,6 @@ from quantipy.core.builds.excel.excel_painter import ExcelPainter -try: - from quantipy.core.builds.powerpoint.pptx_painter import PowerPointPainter -except: - # print ("You do not have the required resources to use PowerPointPainter") - pass +from quantipy.core.builds.powerpoint.pptx_painter import PowerPointPainter from quantipy.version import version as __version__ \ No newline at end of file From 0aa78b2b26c39dc6ee60074486a1445826c1ed07 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Tue, 2 Aug 2016 17:04:38 +0100 Subject: [PATCH 017/733] round values to int before checking if their the same --- core/builds/powerpoint/pptx_painter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index e63f00a38..c1149b132 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -262,7 +262,7 @@ def all_same(val_array): # check if val_array is a numpy array if type(val_array).__module__ == np.__name__: val = val_array.tolist() - return all(x == val[0] for x in val) + return all(round(x[0]) == round(val[0][0]) for x in val) else: raise Exception('This function only takes a numpy array') From f4a05dfa386aaa1c93396e603587263bb1008975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 5 Aug 2016 09:14:36 +0200 Subject: [PATCH 018/733] Test --- core/builds/powerpoint/pptx_painter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index c1149b132..c8d818339 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -264,6 +264,7 @@ def all_same(val_array): val = val_array.tolist() return all(round(x[0]) == round(val[0][0]) for x in val) else: + print 'Kerstin ist die tollste' raise Exception('This function only takes a numpy array') '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' From 71b11d554ed4adc7518ed00b2fb8c41c9f795c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 5 Aug 2016 09:40:55 +0200 Subject: [PATCH 019/733] test back I love Alex --- core/builds/powerpoint/pptx_painter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index c8d818339..c1149b132 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -264,7 +264,6 @@ def all_same(val_array): val = val_array.tolist() return all(round(x[0]) == round(val[0][0]) for x in val) else: - print 'Kerstin ist die tollste' raise Exception('This function only takes a numpy array') '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' From 114699c463a6e5b705877fd71361ac38792e38e5 Mon Sep 17 00:00:00 2001 From: Majeed Sahebzadha Date: Fri, 5 Aug 2016 11:24:30 +0100 Subject: [PATCH 020/733] Handle different types of input. list and list of lists. --- core/builds/powerpoint/pptx_painter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/builds/powerpoint/pptx_painter.py b/core/builds/powerpoint/pptx_painter.py index c1149b132..58b7e0fa5 100644 --- a/core/builds/powerpoint/pptx_painter.py +++ b/core/builds/powerpoint/pptx_painter.py @@ -262,7 +262,12 @@ def all_same(val_array): # check if val_array is a numpy array if type(val_array).__module__ == np.__name__: val = val_array.tolist() - return all(round(x[0]) == round(val[0][0]) for x in val) + if isinstance(val[0], list): + #handle list of lists + return all(round(x[0]) == round(val[0][0]) for x in val) + else: + #handle single list + return all(round(x) == round(val[0]) for x in val) else: raise Exception('This function only takes a numpy array') From 51ba736cf8d5544463f112786390d10a3b3b70c0 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 7 Oct 2016 15:44:48 +0200 Subject: [PATCH 021/733] clean copy of dataset class. --- core/dataset.py | 623 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 509 insertions(+), 114 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index d83232c0b..2d1ca4c36 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -12,13 +12,16 @@ read_ascribe as r_ascribe, write_spss as w_spss, write_quantipy as w_quantipy) + from quantipy.core.helpers.functions import emulate_meta + from quantipy.core.tools.view.logic import ( has_any, has_all, has_count, not_any, not_all, not_count, is_lt, is_ne, is_gt, is_le, is_eq, is_ge, union, intersection, get_logic_index) + from quantipy.core.tools.dp.prep import ( hmerge as _hmerge, vmerge as _vmerge, @@ -33,6 +36,8 @@ import json import warnings +from itertools import product + class DataSet(object): """ A set of casedata (required) and meta data (optional). @@ -153,6 +158,77 @@ def meta(self, name=None, text_key=None): else: return self.describe(name, text_key=text_key) + def variables(self, only_type=None): + """ + Get an overview of all the variables ordered by their type. + + Parameters + ---------- + only_type : str or list of str, default None + Restrict the overview to these data types. + + Returns + ------- + overview : pandas.DataFrame + The variables per data type inside the ``DataSet``. + """ + return self.describe(only_type=only_type) + + def values(self, name, text_key=None): + """ + Parameters + ---------- + name : str + The column variable name keyed in ``_meta['columns']`` or + ``_meta['masks']``. + text_key : str, default None + The text_key that should be used when taking labels from the + source meta. If the given text_key is not found for any + particular text object, the ``DataSet.text_key`` will be used + instead. + + Returns + ------- + vals : ``pandas.DataFrame`` + A summary of value codes and text labels (for provided text_key). + """ + if not self._has_categorical_data(name): + err_msg = '{} does not contain categorical values meta!' + raise TypeError(err_msg.format(name)) + if not text_key: text_key = self.text_key + vals = self.meta(name, text_key)[['codes', 'texts']] + vals = vals.replace('', np.NaN).dropna() + vals.set_index('codes', inplace=True) + vals.columns.name = name + return vals + + def items(self, name, text_key=None): + """ + Parameters + ---------- + name : str + The column variable name keyed in ``_meta['masks']``. + text_key : str, default None + The text_key that should be used when taking labels from the + source meta. If the given text_key is not found for any + particular text object, the ``DataSet.text_key`` will be used + instead. + + Returns + ------- + vals : ``pandas.DataFrame`` + A summary of item names and text labels (for provided text_key). + """ + if not self._is_array(name): + err_msg = '{} is not an array mask!' + raise TypeError(err_msg.format(name)) + if not text_key: text_key = self.text_key + vals = self.meta(name, text_key)[['items', 'item texts']] + vals = vals.replace('', np.NaN).dropna() + vals.set_index('items', inplace=True) + vals.columns.name = name + return vals + def data(self): """ Return the ``data`` component of the ``DataSet`` instance. @@ -190,6 +266,19 @@ def read_quantipy(self, path_meta, path_data): self._set_file_info(path_data, path_meta) return None + def as_delimited_set(self, name): + """ + """ + valid = ['single', 'delimited set'] + if self._is_array(name): + raise NotImplementedError('Cannot switch type on array masks!') + if not self._meta['columns'][name]['type'] in valid: + raise TypeError("'{}' is not of categorical type").format(name) + else: + self._meta['columns'][name]['type'] = 'delimited set' + return None + + def read_dimensions(self, path_meta, path_data): """ Load Dimensions .ddf/.mdd files, connecting as data and meta components. @@ -234,7 +323,7 @@ def read_spss(self, path_sav, **kwargs): source file. """ if path_sav.endswith('.sav'): path_sav = path_sav.replace('.sav', '') - self._meta, self._data = r_spss(path_sav+'.sav', ioLocale=None) + self._meta, self._data = r_spss(path_sav+'.sav', **kwargs) self._set_file_info(path_sav) return None @@ -361,9 +450,9 @@ def from_components(self, data_df, meta_dict=None): def _set_file_info(self, path_data, path_meta=None): self.path = '/'.join(path_data.split('/')[:-1]) + '/' - if path_meta: + try: self.text_key = self._meta['lib']['default text'] - else: + except: self.text_key = None self._data['@1'] = np.ones(len(self._data)) self._meta['columns']['@1'] = {'type': 'int'} @@ -378,16 +467,16 @@ def _show_file_info(self): len(self._data.columns)-1) return None - def list_variables(self, text=False, numeric=False, blacklist=None): + def list_variables(self, numeric=False, text=False, blacklist=None): """ Get list with all variable names except date,boolean,(string,numeric) Parameters ---------- - text : bool, default False - If True, string variables are included in list. numeric : bool, default False If True, int/float variables are included in list. + text : bool, default False + If True, string variables are included in list. blacklist: list of str, Variables that should be excluded @@ -416,9 +505,6 @@ def list_variables(self, text=False, numeric=False, blacklist=None): var_list.append(var_name) return var_list - - - # ------------------------------------------------------------------------ # extending / merging # ------------------------------------------------------------------------ @@ -510,8 +596,8 @@ def update(self, data, on='identity'): def vmerge(self, dataset, on=None, left_on=None, right_on=None, row_id_name=None, left_id=None, right_id=None, row_ids=None, - overwrite_text=False, from_set=None, reset_index=True, - inplace=True, verbose=True): + overwrite_text=False, from_set=None, uniquify_key=None, + reset_index=True, inplace=True, verbose=True): """ Merge Quantipy datasets together by appending rows. @@ -552,6 +638,10 @@ def vmerge(self, dataset, on=None, left_on=None, right_on=None, from_set : str, default=None Use a set defined in the right meta to control which columns are merged from the right dataset. + uniquify_key : str, default None + A int-like column name found in all the passed ``DataSet`` objects + that will be protected from having duplicates. The original version + of the column will be kept under its name prefixed with 'original_'. reset_index : bool, default=True If True pandas.DataFrame.reindex() will be applied to the merged dataframe. @@ -585,13 +675,32 @@ def vmerge(self, dataset, on=None, left_on=None, right_on=None, if inplace: self._data = merged_data self._meta = merged_meta + if uniquify_key: + self._make_unique_key(uniquify_key, 'datasource') return None else: new_dataset = self.copy() new_dataset._data = merged_data new_dataset._meta = merged_meta + if uniquify_key: + new_dataset._make_unique_key(uniquify_key, 'datasource') return new_dataset + def _make_unique_key(self, id_key_name, multiplier): + """ + """ + if not self._meta['columns'][id_key_name]: + raise TypeError("'id_key_name' must be of type int!") + if not self._meta['columns'][multiplier]: + raise TypeError("'multiplier' must be of type int!") + org_key_col = self._data.copy()[id_key_name] + new_name = 'original_{}'.format(id_key_name) + name, qtype, lab = new_name, 'int', 'Original ID' + self.add_meta(name, qtype, lab) + self[new_name] = org_key_col + self[id_key_name] += self[multiplier].astype(int) * 100000000 + return None + def merge_texts(self, dataset): """ Add additional ``text`` versions from other ``text_key`` meta. @@ -723,7 +832,7 @@ def rename(self, name, new_name): self._meta['sets']['data file']['items'] = new_datafile_items return None - def reorder_values(self, name, new_order): + def reorder_values(self, name, new_order=None): """ Apply a new order to the value codes defined by the meta data component. @@ -732,16 +841,21 @@ def reorder_values(self, name, new_order): name : str The column variable name keyed in ``_meta['columns']`` or ``_meta['masks']``. - new_order : list of int - The new code order of the DataSet variable. + new_order : list of int, default None + The new code order of the DataSet variable. If no order is given, + the ``values`` object is sorted ascending. Returns ------- None DataSet is modified inplace. """ - self._verify_old_vs_new_codes(name, new_order) + self._verify_var_in_dataset(name) values = self._get_value_loc(name) + if not new_order: + new_order = list(sorted(self._get_valuemap(name, 'codes'))) + else: + self._verify_old_vs_new_codes(name, new_order) new_values = [value for i in new_order for value in values if value['value'] == i] if self._get_type(name) == 'array': @@ -757,31 +871,72 @@ def remove_values(self, name, remove): Parameters ---------- name : str - The originating column variable name keyed in ``meta['columns']``. + The originating column variable name keyed in ``meta['columns']`` + or ``meta['masks']``. remove : int or list of int - The codes to be removed from the DataSet variable. - + The codes to be removed from the ``DataSet`` variable. Returns ------- None DataSet is modified inplace. """ - if self._is_array(name): - raise NotImplementedError('Cannot remove codes from array masks!') + self._verify_var_in_dataset(name) if not isinstance(remove, list): remove = [remove] values = self._get_value_loc(name) new_values = [value for value in values if value['value'] not in remove] - # LEFT IN FOR LATER - WILL CURRENTLY RAISE WHEN INPUT IS ARRAY if self._get_type(name) == 'array': self._meta['lib']['values'][name] = new_values else: self._meta['columns'][name]['values'] = new_values - if self._is_delimited_set(name): - self._remove_from_delimited_set_data(name, remove) + if self._is_array(name): + items = self._get_itemmap(name, 'items') + for i in items: + self.remove_values(i, remove) else: - self._data.replace(remove, np.NaN, inplace=True) - self._verify_data_vs_meta_codes(name) + if self._is_delimited_set(name): + self._remove_from_delimited_set_data(name, remove) + else: + self._data[name].replace(remove, np.NaN, inplace=True) + self._verify_data_vs_meta_codes(name) + return None + + def remove_items(self, name, remove): + """ + Erase array mask items safely from both meta and case data components. + + Parameters + ---------- + name : str + The originating column variable name keyed in ``meta['masks']``. + remove : int or list of int + The items listed by their order number in the + ``_meta['masks'][name]['items']`` object will be droped from the + ``mask`` definition. + + Returns + ------- + None + DataSet is modified inplace. + """ + if not self._is_array(name): + raise NotImplementedError('Cannot remove items from non-arrays!') + if not isinstance(remove, list): remove = [remove] + items = self._get_itemmap(name, 'items') + drop_item_names = [item for idx, item in enumerate(items, start=1) + if idx in remove] + keep_item_idxs = [idx for idx, item in enumerate(items, start=1) + if idx not in remove] + new_items = self._meta['masks'][name]['items'] + new_items = [item for idx, item in enumerate(new_items, start=1) + if idx in keep_item_idxs] + self._meta['masks'][name]['items'] = new_items + for drop_item_name in drop_item_names: + self._data.drop(drop_item_name, axis=1, inplace=True) + del self._meta['columns'][drop_item_name] + col_ref = 'columns@{}'.format(drop_item_name) + self._meta['sets']['data file']['items'].remove(col_ref) + self._meta['sets'][name]['items'].remove(col_ref) return None def extend_values(self, name, ext_values, text_key=None): @@ -807,9 +962,11 @@ def extend_values(self, name, ext_values, text_key=None): None The ``DataSet`` is modified inplace. """ + self._verify_var_in_dataset(name) is_array = self._is_array(name) if not self._has_categorical_data(name): - raise TypeError('{} does not contain categorical values meta!') + err_msg = '{} does not contain categorical values meta!' + raise TypeError(err_msg.format(name)) if not text_key: text_key = self.text_key if not isinstance(ext_values, list): ext_values = [ext_values] value_obj = self._get_valuemap(name, text_key=text_key) @@ -828,6 +985,9 @@ def extend_values(self, name, ext_values, text_key=None): ext_values = self._make_values_list(ext_values, text_key, start_here) if is_array: self._meta['lib']['values'][name].extend(ext_values) + ext_lib_ref = self._meta['lib']['values'][name] + for item in self._get_itemmap(name, 'items'): + self._meta['columns'][item]['values'] = ext_lib_ref else: self._meta['columns'][name]['values'].extend(ext_values) return None @@ -855,7 +1015,38 @@ def set_text_key(self, text_key): self._meta['lib']['default text'] = text_key return None - def force_texts(self, name=None, copy_to=None, copy_from=None, + + def find_duplicate_texts(self, name, text_key=None): + """ + Collect values that share the same text information to find duplicates. + + Parameters + ---------- + name : str + The column variable name keyed in ``_meta['columns']`` or + ``_meta['masks']``. + text_key : str, default None + Text key for text-based label information. Will automatically fall + back to the instance's ``text_key`` property information if not + provided. + """ + if not text_key: text_key = self.text_key + values = self._get_valuemap(name, text_key=text_key) + dupes_check = [] + text_dupes = [] + for value in values: + if value[1] in dupes_check: + text_dupes.append(value[1]) + dupes_check.append(value[1]) + text_dupes = list(set(text_dupes)) + dupes = [] + for value in values: + if value[1] in text_dupes: + dupes.append(value) + dupes = list(sorted(dupes, key=lambda x: x[1])) + return dupes + + def force_texts(self, name=None, copy_to=None, copy_from=None, update_existing=False, excepts=None): """ Copy info from existing text_key to a new one or update the existing @@ -871,11 +1062,14 @@ def force_texts(self, name=None, copy_to=None, copy_from=None, The text key that will be filled. copy from : str / list {'en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE'} - You can also enter a list with text_keys, if the first text_key + You can also enter a list with text_keys, if the first text_key doesn't exist, it takes the next one update_existing : bool True : copy_to will be filled in any case - False: copy_to will be filled if it's empty/not existing + False: copy_to will be filled if it's empty/not existing + excepts : str or list of str + If provided, the variables passed are ignored while transferring + ``text`` information. Returns ------- @@ -885,7 +1079,7 @@ def _force_texts(tk_dict, copy_to, copy_from, update_existing): if isinstance(tk_dict, dict): new_text_key = None for new_tk in reversed(copy_from): - if new_tk in tk_dict.keys(): + if new_tk in tk_dict.keys(): new_text_key = new_tk if not new_text_key: raise ValueError('{} is no existing text_key'.format(copy_from)) @@ -894,7 +1088,7 @@ def _force_texts(tk_dict, copy_to, copy_from, update_existing): else: if not copy_to in tk_dict.keys(): tk_dict.update({copy_to: tk_dict[new_text_key]}) - return tk_dict + return tk_dict meta = self._meta @@ -939,7 +1133,7 @@ def _force_texts(tk_dict, copy_to, copy_from, update_existing): copy_to=copy_to, copy_from=copy_from, update_existing=update_existing) - if ('values' in column_def.keys() and + if ('values' in column_def.keys() and isinstance(column_def['values'], list)): for no, value in enumerate(column_def['values']): value['text'] = _force_texts(tk_dict= value['text'], @@ -948,6 +1142,7 @@ def _force_texts(tk_dict, copy_to, copy_from, update_existing): update_existing=update_existing) column_def['values'][no]['text'] = value['text'] + @classmethod def _is_valid_text_key(cls, tk): """ @@ -960,7 +1155,7 @@ def _is_valid_text_key(cls, tk): else: return True - def clean_texts(self, clean_html=True, replace_terms=None): + def clean_texts(self, clean_html=True, replace=None): """ Cycle through all meta ``text`` objects replacing unwanted tags/terms. @@ -991,50 +1186,57 @@ def replace_from_dict(obj, tk, replace_map): obj['text'][tk] = text.replace(k, v) meta = self._meta - try: - for mask_name, mask_def in meta['masks'].items(): + for mask_name, mask_def in meta['masks'].items(): + try: for tk in mask_def['text']: text = mask_def['text'][tk] if clean_html: - mask_def['text'][tk] = remove_html(text) - if replace_terms: - replace_from_dict(mask_def, tk, replace_terms) + mask_def['text'][tk] = remove_html(text) + if replace: + replace_from_dict(mask_def, tk, replace) + except: + pass + try: for no, item in enumerate(mask_def['items']): for tk in item['text']: text = item['text'][tk] if clean_html: mask_def['items'][no]['text'][tk] = remove_html(text) - if replace_terms: - replace_from_dict(item, tk, replace_terms) - mask_vals = meta['lib']['values'][mask_name] + if replace: + replace_from_dict(item, tk, replace) + except: + pass + mask_vals = meta['lib']['values'][mask_name] + try: for no, val in enumerate(mask_vals): for tk in val['text']: text = val['text'][tk] if clean_html: mask_vals[no]['text'][tk] = remove_html(text) - if replace_terms: - replace_from_dict(val, tk, replace_terms) - except: - pass + if replace: + replace_from_dict(val, tk, replace) + except: + pass for column_name, column_def in meta['columns'].items(): try: for tk in column_def['text']: text = column_def['text'][tk] if clean_html: column_def['text'][tk] = remove_html(text) - if replace_terms: - replace_from_dict(column_def, tk, replace_terms) + if replace: + replace_from_dict(column_def, tk, replace) if 'values' in column_def: for no, value in enumerate(column_def['values']): for tk in value['text']: text = value['text'][tk] if clean_html: column_def['values'][no]['text'][tk] = remove_html(text) - if replace_terms: - replace_from_dict(value, tk, replace_terms) + if replace: + replace_from_dict(value, tk, replace) except: pass + def set_value_texts(self, name, renamed_vals, text_key=None): """ Rename or add value texts in the 'values' object. @@ -1060,8 +1262,10 @@ def set_value_texts(self, name, renamed_vals, text_key=None): None The ``DataSet`` is modified inplace. """ + self._verify_var_in_dataset(name) if not self._has_categorical_data(name): - raise TypeError('{} does not contain categorical values meta!') + err_msg = '{} does not contain categorical values meta!' + raise TypeError(err_msg.format(name)) if not text_key: text_key = self.text_key if not self._is_array(name): @@ -1289,31 +1493,54 @@ def _add_array(self, name, qtype, label, items, categories, text_key, dims_like) self._meta['sets'][array_name] = {'items': [i['source'] for i in item_objects]} return None - def copy_var(self, name, suffix='rec'): + def copy_var(self, name, suffix='rec', copy_data=True): """ Copy meta and case data of the variable defintion given per ``name``. Parameters ---------- name : str - The originating column variable name keyed in ``meta['columns']``. + The originating column variable name keyed in ``meta['columns']`` + or ``meta['masks']``. suffix : str, default 'rec' The new variable name will be constructed by suffixing the original ``name`` with ``_suffix``, e.g. ``'age_rec``. - Returns ------- None DataSet is modified inplace, adding a copy to both the data and meta component. """ - if self._is_array(name): - raise NotImplementedError('Cannot copy array masks!') + self._verify_var_in_dataset(name) copy_name = '{}_{}'.format(name, suffix) - self._data[copy_name] = self._data[name].copy() - meta_copy = copy.deepcopy(self._meta['columns'][name]) - self._meta['columns'][copy_name] = meta_copy - self._meta['sets']['data file']['items'].append('columns@' + copy_name) + if self._is_array(name): + items = self._get_itemmap(name, 'items') + mask_meta_copy = org_copy.deepcopy(self._meta['masks'][name]) + if not 'masks@' + copy_name in self._meta['sets']['data file']['items']: + self._meta['sets']['data file']['items'].append('masks@' + copy_name) + mask_set = [] + for i, i_meta in zip(items, mask_meta_copy['items']): + self.copy_var(i, suffix, copy_data) + i_name = '{}_{}'.format(i, suffix) + i_meta['source'] = 'columns@{}'.format(i_name) + mask_set.append('columns@{}'.format(i_name)) + lib_ref = 'lib@values@{}'.format(copy_name) + lib_copy = org_copy.deepcopy(self._meta['lib']['values'][name]) + mask_meta_copy['values'] = lib_ref + self._meta['masks'][copy_name] = mask_meta_copy + self._meta['lib']['values'][copy_name] = lib_copy + self._meta['sets'][copy_name] = {'items': mask_set} + else: + if copy_data: + self._data[copy_name] = self._data[name].copy() + else: + self._data[copy_name] = np.NaN + meta_copy = org_copy.deepcopy(self._meta['columns'][name]) + self._meta['columns'][copy_name] = meta_copy + self._meta['columns'][copy_name]['name'] = copy_name + if not 'columns@' + copy_name in self._meta['sets']['data file']['items']: + self._meta['sets']['data file']['items'].append('columns@' + copy_name) + return None def crosstab(self, x, y=None, w=None, pct=False, decimals=1, text=True, rules=False, xtotal=False): @@ -1354,7 +1581,30 @@ def _item(item_name, text_key, text): def _make_items_object(self, item_definition, text_key): pass - def unify_values(self, name, code_map): + def copy_array_data(self, source, target, source_items=None, + target_items=None, slicer=None): + """ + """ + self._verify_same_value_codes_meta(source, target) + all_source_items = self._get_itemmap(source, non_mapped='items') + all_target_items = self._get_itemmap(target, non_mapped='items') + if slicer: mask = self.slicer(slicer) + if source_items: + source_items = [all_source_items[i-1] for i in source_items] + else: + source_items = all_source_items + if target_items: + target_items = [all_target_items[i-1] for i in target_items] + else: + target_items = all_target_items + for s, t in zip(source_items, target_items): + if slicer: + self._data.loc[mask, t] = self._data.loc[mask, s] + else: + self[t] = self[s] + return None + + def unify_values(self, name, code_map, slicer=None, exclusive=False): """ Use a mapping of old to new codes to replace code values in ```_data``. @@ -1367,14 +1617,28 @@ def unify_values(self, name, code_map): code_map : dict A mapping of ``{old: new}``; ``old`` and ``new`` must be the int-type code values from the column meta data. + slicer : Quantipy logic statement, default None + If provided, the values will only be unified for cases where the + condition holds. + exclusive : bool, default False + If True, the recoded unified value will replace whatever is already + found in the ``_data`` column, ignoring ``delimited set`` typed data + to which normally would get appended to. Returns ------- None """ + append = self._is_delimited_set(name) + if exclusive: append = False for old_code, new_code in code_map.items(): - self.recode(name, {new_code: {name: [old_code]}}) - self.remove_values(name, old_code) + self.recode(name, {new_code: {name: [old_code]}}, + append=append, intersect=slicer) + if not slicer: + self.remove_values(name, old_code) + else: + msg = "Unified {} >> {} on data slice. Remove values meta if needed!" + print msg.format(old_code, new_code) return None @staticmethod @@ -1402,27 +1666,8 @@ def _verify_same_value_codes_meta(self, name_a, name_b): raise ValueError(msg.format(name_a, name_b)) return None - def copy_array_data(self, source, target, source_items=None, - target_items=None): - """ - """ - self._verify_same_value_codes_meta(source, target) - all_source_items = self._get_itemmap(source, non_mapped='items') - all_target_items = self._get_itemmap(target, non_mapped='items') - if source_items: - source_items = [all_source_items[i-1] for i in source_items] - else: - source_items = all_source_items - if target_items: - target_items = [all_target_items[i-1] for i in target_items] - else: - target_items = all_target_items - for s, t in zip(source_items, target_items): - self[t] = self[s] - return None - def transpose_array(self, name, new_name=None, ignore_items=None, - ignore_values=None, text_key=None): + ignore_values=None, copy_data=True, text_key=None): """ Create a new array mask with transposed items / values structure. @@ -1436,7 +1681,7 @@ def transpose_array(self, name, new_name=None, ignore_items=None, new_name : str, default None The name of the new mask. If not provided explicitly, the new_name will be constructed constructed by suffixing the original - ``name`` with ``_suffix``, e.g. ``'Q2Array_trans``. + ``name`` with '_trans', e.g. ``'Q2Array_trans``. ignore_items : int or list of int, default None If provided, the items listed by their order number in the ``_meta['masks'][name]['items']`` object will not be part of the @@ -1492,7 +1737,7 @@ def transpose_array(self, name, new_name=None, ignore_items=None, else: dimensions_like = False if not new_name: - new_name = '{}_{}'.format(name, suffix) + new_name = '{}_trans'.format(name) # Create the new meta data entry for the transposed array structure qtype = 'delimited set' @@ -1512,9 +1757,11 @@ def transpose_array(self, name, new_name=None, ignore_items=None, self[trans_item] = '' else: self[trans_item] = np.NaN - slicer = {reg_item_name: [reg_val_code]} - update_with = new_val_code - self.fill_conditional(trans_item, slicer, update_with) + if copy_data: + slicer = {reg_item_name: [reg_val_code]} + update_with = new_val_code + self.recode(trans_item, {update_with: slicer}, + append=True) print 'Transposed array: {} into {}'.format(org_name, new_name) def slicer(self, condition): @@ -1647,6 +1894,30 @@ def recode(self, target, mapper, default=None, append=False, else: return recode_series + def interlock(self, name, label, variables, val_text_sep = '/'): + """ + """ + if not isinstance(variables, list) or len(variables) < 2: + raise ValueError("'variables' must be a list of at least two items!") + if any(self._is_delimited_set(v) for v in variables): + qtype = 'delimited set' + else: + qtype = 'single' + codes = [self._get_valuemap(v, 'codes') for v in variables] + texts = [self._get_valuemap(v, 'texts') for v in variables] + zipped = zip(list(product(*codes)), list(product(*texts))) + categories = [] + cat_id = 0 + for codes, texts in zipped: + cat_id += 1 + label = val_text_sep.join(texts) + rec = [{v: [c]} for v, c in zip(variables, codes)] + rec = intersection(rec) + categories.append((cat_id, label, rec)) + self.derive_categorical(name, qtype, label, categories) + return None + + def derive_categorical(self, name, qtype, label, cond_map, text_key=None): """ Create meta and recode case data by specifying derived category logics. @@ -1893,21 +2164,7 @@ def _remove_from_delimited_set_data(self, name, remove): self._data[name] = data return None - def variables(self, only_type=None): - """ - Get an overview of all the variables ordered by their type. - Parameters - ---------- - only_type : str or list of str, default None - Restrict the overview to these data types. - - Returns - ------- - overview : pandas.DataFrame - The variables per data type inside the ``DataSet``. - """ - return self.describe(only_type=only_type) def describe(self, var=None, only_type=None, text_key=None): """ @@ -2064,7 +2321,7 @@ def _has_categorical_data(self, name): else: return False - def _verify_data_vs_meta_codes(self, name): + def _verify_data_vs_meta_codes(self, name, raiseError=True): """ """ if self._is_delimited_set(name): @@ -2079,10 +2336,11 @@ def _verify_data_vs_meta_codes(self, name): msg = "Warning: Meta not consistent with case data for '{}'!" print '*' * 60 print msg.format(name) - print '*' * 60 + if raiseError: print '*' * 60 print 'Found in data: {}'.format(data_codes) print 'Defined as per meta: {}'.format(meta_codes) - raise ValueError('Please review your data processing!') + if raiseError: + raise ValueError('Please review your data processing!') return None def _verify_old_vs_new_codes(self, name, new_codes): @@ -2113,6 +2371,7 @@ def _verify_column_in_meta(self, name): if n not in self._meta['columns']: raise KeyError("'{}' not found in meta data!".format(n)) return None + def _get_label(self, var, text_key=None): if text_key is None: text_key = self.text_key if self._get_type(var) == 'array': @@ -2135,7 +2394,7 @@ def _get_value_loc(self, var): else: return emulate_meta(self._meta, loc[var]) - def _get_valuemap(self, var, non_mapped=None, text_key=None): + def _get_valuemap(self, var, non_mapped=None, text_key=None): if text_key is None: text_key = self.text_key vals = self._get_value_loc(var) if non_mapped in ['codes', 'lists', None]: @@ -2169,15 +2428,19 @@ def _get_itemmap(self, var, non_mapped=None, text_key=None): else: return zip(items, items_texts) + def _verify_var_in_dataset(self, name): + if not name in self._meta['masks'] and not name in self._meta['columns']: + raise KeyError("'{}' not found in DataSet!".format(name)) + def _get_meta(self, var, type=None, text_key=None): - if not var in self._meta['masks'] and not var in self._meta['columns']: - raise KeyError("'{}' not found in DataSet!".format(var)) + self._verify_var_in_dataset(var) if text_key is None: text_key = self.text_key var_type = self._get_type(var) label = self._get_label(var, text_key) missings = self._get_missing_map(var) if not self._is_numeric(var): - codes, texts = self._get_valuemap(var, non_mapped='lists') + codes, texts = self._get_valuemap(var, non_mapped='lists', + text_key=text_key) if missings: codes_copy = codes[:] for miss_types, miss_codes in missings.items(): @@ -2188,7 +2451,8 @@ def _get_meta(self, var, type=None, text_key=None): else: missings = [None] * len(codes) if var_type == 'array': - items, items_texts = self._get_itemmap(var, non_mapped='lists') + items, items_texts = self._get_itemmap(var, non_mapped='lists', + text_key=text_key) idx_len = max((len(codes), len(items))) if len(codes) > len(items): pad = (len(codes) - len(items)) @@ -2280,10 +2544,7 @@ def filter(self, alias, condition, inplace=False): """ Filter the DataSet using a Quantipy logical expression. """ - if not inplace: - data = self._data.copy() - else: - data = self._data + data = self._data.copy() filter_idx = get_logic_index(pd.Series(data.index), condition, data) filtered_data = data.iloc[filter_idx[0], :] if inplace: @@ -2294,6 +2555,7 @@ def filter(self, alias, condition, inplace=False): new_ds._data = filtered_data new_ds._meta = self._meta new_ds.filtered = alias + new_ds.text_key = self.text_key return new_ds # ------------------------------------------------------------------------ @@ -2306,4 +2568,137 @@ def link(self, filters=None, x=None, y=None, views=None): #raise NotImplementedError('Links from DataSet currently not supported!') if filters is None: filters = 'no_filter' l = qp.sandbox.Link(self, filters, x, y) - return l \ No newline at end of file + return l + + + # ------------------------------------------------------------------------ + # validate the dataset + # ------------------------------------------------------------------------ + + def validate(self, text=True, categorical=True, codes=True): + """ + Validates variables/ text objects/ ect in the dataset + """ + + meta = self._meta + data = self._data + + # validate text-objects + if text: + self.validate_text_objects(test_object=None, name=None) + + # validate categorical objects (single, delimited set, array) + if codes: categorical = True + if categorical: + error_list = self.validate_categorical_objects() + + # validate data vs meta codes + if codes: + for key in data.keys(): + if key.startswith('id_') or key in error_list: continue + elif self._has_categorical_data(key): + self._verify_data_vs_meta_codes(key, raiseError=False) + + + + def _proof_values(self, variable, name, error_list): + msg = "Warning: Meta is not consistent for '{}'!" + + if not 'values' in variable.keys(): + error_list.append(name) + print '*' * 60 + print msg.format(name) + print "Meta doesn't contain any codes" + return error_list + + values = variable['values'] + if isinstance(values, list): + return error_list + elif (isinstance(values, basestring) and + values.split('@')[-1] in self._meta['lib']['values']): + return error_list + else: + error_list.append(name) + print '*' * 60 + print msg.format(name) + print "Codes are not a list or reference doesn't exist" + return error_list + + + def validate_categorical_objects(self): + + meta = self._meta + data = self._data + + error_list = [] + + # validate delimited set, single + for col in meta['columns']: + var = meta['columns'][col] + if var['type'] in ['delimited set', 'single']: + error_list = self._proof_values(variable=var, name=col, + error_list=error_list) + + # validate array + for mask in meta['masks']: + arr = meta['masks'][mask] + error_list = self._proof_values(variable=arr, name=mask, + error_list=error_list) + for item in arr['items']: + ref = item['source'].split('@') + if ref[-1] in meta[ref[0]]: continue + else: + print '*' * 60 + print "Warning: Meta is not consistent for '{}'!".format(mask) + print "Source reference {} doesn't exist".format(ref[-1]) + + return error_list + + + @classmethod + def _validate_text_objects(cls, test_object, text_key, name): + + msg = "Warning: Text object is not consistent: '{}'!" + if not isinstance(test_object, dict): + print '*' * 60 + print msg.format(name) + print 'Text object is not a dict' + elif text_key not in test_object.keys(): + print '*' * 60 + print msg.format(name) + print 'Text object does not contain dataset-text_key {}'.format( + text_key) + elif test_object[text_key] in [None, '', ' ']: + print '*' * 60 + print msg.format(name) + print 'Text object has empty text mapping' + + + def validate_text_objects(self, test_object, name=None): + """ + Prove all text objects in the dataset. + """ + + meta = self._meta + text_key = self.text_key + + if test_object == None : test_object= self._meta + if name == None: + name = 'meta' + + if isinstance(test_object, dict): + for key in test_object.keys(): + new_name = name + '[' + key + ']' + if 'text' == key: + self._validate_text_objects(test_object['text'], + text_key, new_name) + elif (key in ['properties', 'data file'] or + 'qualityControl' in key): + continue + else: + self.validate_text_objects(test_object[key], new_name) + elif isinstance(test_object, list): + for i, item in enumerate(test_object): + new_name = name + '[' + str(i) + ']' + self.validate_text_objects(item,new_name) + From d9f1d894fa6b75618b1920b3066ef18086667c65 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 7 Oct 2016 15:59:50 +0200 Subject: [PATCH 022/733] fixing indent. --- core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/dataset.py b/core/dataset.py index 2d1ca4c36..8bf40dbfd 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -1046,7 +1046,7 @@ def find_duplicate_texts(self, name, text_key=None): dupes = list(sorted(dupes, key=lambda x: x[1])) return dupes - def force_texts(self, name=None, copy_to=None, copy_from=None, + def force_texts(self, name=None, copy_to=None, copy_from=None, update_existing=False, excepts=None): """ Copy info from existing text_key to a new one or update the existing From 541b94a4254ba9cabfe071cefc6d332f9a5c99c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 11 Oct 2016 08:02:00 +0200 Subject: [PATCH 023/733] first draft: alternative solution unittest is missing --- core/dataset.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/core/dataset.py b/core/dataset.py index 7f0904db4..c3f5e6842 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2573,6 +2573,152 @@ def link(self, filters=None, x=None, y=None, views=None): # validate the dataset # ------------------------------------------------------------------------ + def validate_output(self): + + def err_appender(text, err_var, app, count, text_key): + if not isinstance(text, dict): + if err_var[0] == '': err_var[0] += app + count + elif err_var[0] == 'x': err_var[0] += ', ' +app + count + else: err_var[0] += ', ' + count + elif self.text_key not in text: + if err_var[1] == '': err_var[1] += app + count + elif err_var[1] == 'x': err_var[1] += ', ' +app + count + else: err_var[1] += ', ' + count + elif text[text_key] in [None, '', ' ']: + if err_var[2] == '': err_var[2] += app + count + elif err_var[2] == 'x': err_var[2] += ', ' +app + count + else: err_var[2] += ', ' + count + return err_var + + + def append_loop(err_var_item, app, count): + if app in err_var_item: + err_var_item += ', ' + str(count) + else: + err_var_item += app + ' ' + str(count) + return err_var_item + + def data_vs_meta_codes(name): + if not name in self._data: return False + if self._is_delimited_set(name): + data_codes = self._data[name].str.get_dummies(';').columns.tolist() + data_codes = [int(c) for c in data_codes] + else: + data_codes = pd.get_dummies(self._data[name]).columns.tolist() + meta_codes = self._get_valuemap(name, non_mapped='codes') + wild_codes = [code for code in data_codes if code not in meta_codes] + return wild_codes + + meta = self._meta + data = self._data + + text_key = self.text_key + + msg = ("Error explanations:\n" + "\tErr1: Text object is not a dict.\n" + "\tErr2: Text onject doesn't contain dataset text_key '{}'.\n" + "\tErr3: Text object has empty text mapping.\n" + "\tErr4: Categorical object doesn't contain any 'Values'.\n" + "\tErr5: Categorical object has badly formatted 'Values'.\n" + "\t\t (not a list or reference doesn't exsist)\n" + "\tErr6: 'Source' reference doesn't exsist.\n" + "\tErr7: 'Data' contains codes that are not in 'Meta'.\n").format( + text_key) + + err_columns = ['Err{}'.format(x) for x in range(1,8)] + err_df = pd.DataFrame(columns=err_columns) + + for ma in meta['masks']: + if ma.startswith('qualityControl_'): continue + + err_var = ['' for x in range(7)] + + mask = meta['masks'][ma] + if 'text' in mask: + text = mask['text'] + err_var = err_appender(text, err_var, '', 'x', text_key) + for x, item in enumerate(mask['items']): + if 'text' in item: + text = item['text'] + err_var = err_appender(text, err_var, 'item ', + str(x), text_key) + if 'source' in item: + if (isinstance(item['source'], basestring) + and '@' in item['source']): + try: + ref = item['source'].split('@') + if not ref[-1] in meta[ref[0]]: + err_var[5] = append_loop(err_var[5], + 'item ', x) + except: + err_var[5] = append_loop(err_var[5], 'item ', x) + else: + err_var[5] = append_loop(err_var[5], 'item ', x) + if not 'values' in mask: + err_var[3] = 'x' + elif not (isinstance(mask['values'], list) or + isinstance(mask['values'], basestring) and + mask['values'].split('@')[-1] in meta['lib']['values']): + err_var[4] = 'x' + + if not ('').join(err_var) == '': + new_err = pd.DataFrame([err_var], index=[ma], + columns=err_columns) + err_df = err_df.append(new_err) + + excepts = [col for col in data if col not in meta['columns']] + + for col in meta['columns']: + if col.startswith('qualityControl_'): continue + + err_var = ['' for x in range(7)] + + column = meta['columns'][col] + if 'text' in column: + text = column['text'] + err_var = err_appender(text, err_var, '', 'x', text_key) + if 'values' in column: + if not (isinstance(column['values'], list) or + isinstance(column['values'], basestring) and + column['values'].split('@')[-1] in meta['lib']['values']): + err_var[4] = 'x' + for x, val in enumerate(column['values']): + if 'text' in val: + text = val['text'] + err_var = err_appender(text, err_var, 'value ', + str(x), text_key) + elif ('values' not in column and + column['type'] in ['delimited set', 'single']): + err_var[3] = 'x' + + + if (self._has_categorical_data(col) and err_var[3] == '' and + err_var[4] == ''): + if data_vs_meta_codes(col): + err_var[6] = 'x' + + + + if not ('').join(err_var) == '': + new_err = pd.DataFrame([err_var], index=[col], + columns=err_columns) + err_df = err_df.append(new_err) + + for col in excepts: + if col.startswith('id_'): continue + else: + new_err = pd.DataFrame([['x' for x in range(7)]], index=[col], + columns=err_columns) + err_df = err_df.append(new_err) + + + if not len(err_df) == 0: + print msg + return err_df.sort() + else: + print 'no issues found in dataset' + + def validate(self, text=True, categorical=True, codes=True): """ Validates variables/ text objects/ ect in the dataset From 843899dd471c917b0573cdab912df6d78053d951 Mon Sep 17 00:00:00 2001 From: James Griffiths Date: Mon, 24 Oct 2016 12:55:38 -0400 Subject: [PATCH 024/733] Catch mixed-cap element names when generating mask item labels. --- core/tools/dp/dimensions/reader.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/tools/dp/dimensions/reader.py b/core/tools/dp/dimensions/reader.py index 796d9f652..21af4a745 100644 --- a/core/tools/dp/dimensions/reader.py +++ b/core/tools/dp/dimensions/reader.py @@ -603,17 +603,19 @@ def get_columns_meta(xml, meta, data, map_values=True): }) try: - xpath_element = "//design//category[@name='%s']//properties" % (tmap[1]) - sources = xml.xpath( - xpath_grid+"//categories//category[@name='%s']//labels//text" % (tmap[1])) + xpath_properties = "//design//category[@name='%s']//properties" % (tmap[1]) + xpath_labels = xpath_grid+"//categories//category[@name='%s']//labels//text" % (tmap[1]) + sources = xml.xpath(xpath_labels) if not sources: - sources = xml.xpath( - xpath_grid+"//categories//category[@name='%s']//labels//text" % ( - tmap[1].upper())) + xpath_labels = xpath_grid+"//categories//category[@name='%s']//labels//text" % (tmap[1].upper()) + sources = xml.xpath(xpath_labels) if not sources: - sources = xml.xpath( - xpath_grid+"//categories//category[@name='%s']//labels//text" % ( - tmap[1].lower())) + xpath_labels = xpath_grid+"//categories//category[@name='%s']//labels//text" % (tmap[1].lower()) + sources = xml.xpath(xpath_labels) + if not sources: + mixed_cap = "{}{}".format(tmap[0], tmap[1][len(tmap[0]):]) + xpath_labels = xpath_grid+"//categories//category[@name='%s']//labels//text" % (mixed_cap) + sources = xml.xpath(xpath_labels) element_text = { source.get('{http://www.w3.org/XML/1998/namespace}lang'): "" if source.text is None else source.text @@ -622,10 +624,12 @@ def get_columns_meta(xml, meta, data, map_values=True): element_text = tmap[1] if element_text is None: element_text = "" + print col_name, element_text + print sources meta['masks'][mm_name]['items'].append({ 'source': 'columns@%s' % col_name, 'text': element_text, - 'properties': get_meta_properties(xml, xpath_element) + 'properties': get_meta_properties(xml, xpath_properties) }) else: From 17be083bd5ba5bf5fa1e290e19f5ef2ac3e4f7b4 Mon Sep 17 00:00:00 2001 From: James Griffiths Date: Mon, 24 Oct 2016 12:56:59 -0400 Subject: [PATCH 025/733] Remove debugging print lines. --- core/tools/dp/dimensions/reader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/tools/dp/dimensions/reader.py b/core/tools/dp/dimensions/reader.py index 21af4a745..c45cbb40d 100644 --- a/core/tools/dp/dimensions/reader.py +++ b/core/tools/dp/dimensions/reader.py @@ -624,8 +624,6 @@ def get_columns_meta(xml, meta, data, map_values=True): element_text = tmap[1] if element_text is None: element_text = "" - print col_name, element_text - print sources meta['masks'][mm_name]['items'].append({ 'source': 'columns@%s' % col_name, 'text': element_text, From 24151eb542ef835779fe0ec2bb2c458c7db15440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 25 Oct 2016 15:52:29 +0200 Subject: [PATCH 026/733] improved validate function and unit test --- core/dataset.py | 16 +++++++++------- tests/test_dataset.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index c3f5e6842..1d1a381e7 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2573,7 +2573,7 @@ def link(self, filters=None, x=None, y=None, views=None): # validate the dataset # ------------------------------------------------------------------------ - def validate_output(self): + def validate(self, verbose=True): def err_appender(text, err_var, app, count, text_key): if not isinstance(text, dict): @@ -2711,15 +2711,17 @@ def data_vs_meta_codes(name): columns=err_columns) err_df = err_df.append(new_err) - - if not len(err_df) == 0: - print msg - return err_df.sort() + if verbose: + if not len(err_df) == 0: + print msg + return err_df.sort() + else: + print 'no issues found in dataset' else: - print 'no issues found in dataset' + return err_df.sort() - def validate(self, text=True, categorical=True, codes=True): + def validate_backup(self, text=True, categorical=True, codes=True): """ Validates variables/ text objects/ ect in the dataset """ diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 6684dbb90..75ec63fbc 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -243,3 +243,29 @@ def test_force_texts(self): self.assertRaises(ValueError, dataset.force_texts, name='q4', copy_from=['sv-SE']) + + def test_validate(self): + dataset = self._get_dataset() + meta = dataset._meta + meta['columns']['q1'].pop('values') + meta['columns'].pop('q2') + meta['masks']['q5']['items'][1]['source'] = '' + meta['masks']['q6']['text'].pop('en-GB') + meta['lib']['values'].pop('q6') + meta['columns']['q8']['text'] = '' + meta['columns']['q8']['values'][3]['text'] = '' + meta['columns']['q8']['values'] = meta['columns']['q8']['values'][0:5] + index = ['q1', 'q2', 'q5', 'q6', 'q6_1', 'q6_2', 'q6_3', 'q7', 'q8'] + data = {'Err1': ['', 'x', '', '', '', '', '', '', 'x, value 3'], + 'Err2': ['', 'x', '', 'x', '', '', '', '', ''], + 'Err3': ['', 'x', 'x', '', '', '', '', 'x', ''], + 'Err4': ['x', 'x', '', '', '', '', '', '', ''], + 'Err5': ['', 'x', '', 'x', 'x', 'x', 'x', '', ''], + 'Err6': ['', 'x', 'item 1', '', '', '', '', '', ''], + 'Err7': ['', 'x', '', '', '', '', '', '', 'x']} + df = pd.DataFrame(data, index = index) + df_validate = dataset.validate(verbose = False) + self.assertTrue(df.equals(df_validate)) + + + From 1bb549f924f2507c7dfa13a566577755562208c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 27 Oct 2016 11:52:41 +0200 Subject: [PATCH 027/733] adding doc string --- core/dataset.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/dataset.py b/core/dataset.py index 1d1a381e7..8477be1a6 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2574,6 +2574,9 @@ def link(self, filters=None, x=None, y=None, views=None): # ------------------------------------------------------------------------ def validate(self, verbose=True): + """ + Identify and report inconsistencies in the ``DataSet`` instance. + """ def err_appender(text, err_var, app, count, text_key): if not isinstance(text, dict): From 077332220ab8cc85e00d66ebb3784a21752dd6b4 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 28 Oct 2016 13:52:50 +0200 Subject: [PATCH 028/733] getting rid of some typos in validate. --- core/dataset.py | 52 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index 8477be1a6..2704d67a9 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2579,7 +2579,7 @@ def validate(self, verbose=True): """ def err_appender(text, err_var, app, count, text_key): - if not isinstance(text, dict): + if not isinstance(text, dict): if err_var[0] == '': err_var[0] += app + count elif err_var[0] == 'x': err_var[0] += ', ' +app + count else: err_var[0] += ', ' + count @@ -2587,7 +2587,7 @@ def err_appender(text, err_var, app, count, text_key): if err_var[1] == '': err_var[1] += app + count elif err_var[1] == 'x': err_var[1] += ', ' +app + count else: err_var[1] += ', ' + count - elif text[text_key] in [None, '', ' ']: + elif text[text_key] in [None, '', ' ']: if err_var[2] == '': err_var[2] += app + count elif err_var[2] == 'x': err_var[2] += ', ' +app + count else: err_var[2] += ', ' + count @@ -2598,7 +2598,7 @@ def append_loop(err_var_item, app, count): if app in err_var_item: err_var_item += ', ' + str(count) else: - err_var_item += app + ' ' + str(count) + err_var_item += app + ' ' + str(count) return err_var_item def data_vs_meta_codes(name): @@ -2619,20 +2619,20 @@ def data_vs_meta_codes(name): msg = ("Error explanations:\n" "\tErr1: Text object is not a dict.\n" - "\tErr2: Text onject doesn't contain dataset text_key '{}'.\n" + "\tErr2: Text object does not contain dataset text_key '{}'.\n" "\tErr3: Text object has empty text mapping.\n" - "\tErr4: Categorical object doesn't contain any 'Values'.\n" + "\tErr4: Categorical object does not contain any 'Values'.\n" "\tErr5: Categorical object has badly formatted 'Values'.\n" - "\t\t (not a list or reference doesn't exsist)\n" - "\tErr6: 'Source' reference doesn't exsist.\n" - "\tErr7: 'Data' contains codes that are not in 'Meta'.\n").format( - text_key) + "\t\t (not a list or reference does not exist)\n" + "\tErr6: 'Source' reference does not exist.\n" + "\tErr7: '._data' contains codes that are not in ._meta.\n") + msg = msg.format(text_key) err_columns = ['Err{}'.format(x) for x in range(1,8)] err_df = pd.DataFrame(columns=err_columns) for ma in meta['masks']: - if ma.startswith('qualityControl_'): continue + if ma.startswith('qualityControl_'): continue err_var = ['' for x in range(7)] @@ -2641,17 +2641,17 @@ def data_vs_meta_codes(name): text = mask['text'] err_var = err_appender(text, err_var, '', 'x', text_key) for x, item in enumerate(mask['items']): - if 'text' in item: + if 'text' in item: text = item['text'] err_var = err_appender(text, err_var, 'item ', str(x), text_key) if 'source' in item: - if (isinstance(item['source'], basestring) + if (isinstance(item['source'], basestring) and '@' in item['source']): try: ref = item['source'].split('@') if not ref[-1] in meta[ref[0]]: - err_var[5] = append_loop(err_var[5], + err_var[5] = append_loop(err_var[5], 'item ', x) except: err_var[5] = append_loop(err_var[5], 'item ', x) @@ -2659,20 +2659,20 @@ def data_vs_meta_codes(name): err_var[5] = append_loop(err_var[5], 'item ', x) if not 'values' in mask: err_var[3] = 'x' - elif not (isinstance(mask['values'], list) or + elif not (isinstance(mask['values'], list) or isinstance(mask['values'], basestring) and mask['values'].split('@')[-1] in meta['lib']['values']): err_var[4] = 'x' - + if not ('').join(err_var) == '': - new_err = pd.DataFrame([err_var], index=[ma], + new_err = pd.DataFrame([err_var], index=[ma], columns=err_columns) err_df = err_df.append(new_err) excepts = [col for col in data if col not in meta['columns']] for col in meta['columns']: - if col.startswith('qualityControl_'): continue + if col.startswith('qualityControl_'): continue err_var = ['' for x in range(7)] @@ -2680,30 +2680,30 @@ def data_vs_meta_codes(name): if 'text' in column: text = column['text'] err_var = err_appender(text, err_var, '', 'x', text_key) - if 'values' in column: + if 'values' in column: if not (isinstance(column['values'], list) or isinstance(column['values'], basestring) and column['values'].split('@')[-1] in meta['lib']['values']): err_var[4] = 'x' for x, val in enumerate(column['values']): - if 'text' in val: + if 'text' in val: text = val['text'] err_var = err_appender(text, err_var, 'value ', str(x), text_key) - elif ('values' not in column and + elif ('values' not in column and column['type'] in ['delimited set', 'single']): err_var[3] = 'x' - - if (self._has_categorical_data(col) and err_var[3] == '' and + + if (self._has_categorical_data(col) and err_var[3] == '' and err_var[4] == ''): if data_vs_meta_codes(col): err_var[6] = 'x' - - + + if not ('').join(err_var) == '': - new_err = pd.DataFrame([err_var], index=[col], + new_err = pd.DataFrame([err_var], index=[col], columns=err_columns) err_df = err_df.append(new_err) @@ -2722,7 +2722,7 @@ def data_vs_meta_codes(name): print 'no issues found in dataset' else: return err_df.sort() - + def validate_backup(self, text=True, categorical=True, codes=True): """ From 3e18b4720fd16476b30caa12451781f4776d8ebb Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Tue, 27 Dec 2016 16:42:04 +0100 Subject: [PATCH 029/733] removing print statement. --- core/stack.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/stack.py b/core/stack.py index 265074738..41ddac4e3 100644 --- a/core/stack.py +++ b/core/stack.py @@ -444,7 +444,6 @@ def get_chain(self, name=None, data_keys=None, filters=None, x=None, y=None, view_df = view_df[rules_y_slicer] if vk.split('|')[1].startswith('t.'): view_df = verify_test_results(view_df) - print view_df chain_view = View( link=stack_link, name = stack_view.name, From 9ced1a3fe8ac7e6b1f99cb28f411df6737af0659 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Wed, 4 Jan 2017 10:23:11 +0100 Subject: [PATCH 030/733] removing debug print. --- core/stack.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/stack.py b/core/stack.py index aadde188a..969fbecda 100644 --- a/core/stack.py +++ b/core/stack.py @@ -446,7 +446,6 @@ def get_chain(self, name=None, data_keys=None, filters=None, x=None, y=None, view_df = view_df[rules_y_slicer] if vk.split('|')[1].startswith('t.'): view_df = verify_test_results(view_df) - print view_df chain_view = View( link=stack_link, name = stack_view.name, From bf1b06085af8e9520164eb09fdd785706ff7f40b Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Wed, 4 Jan 2017 15:21:05 +0100 Subject: [PATCH 031/733] gitignore for doc files --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 46f69c36a..105c18f36 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ core/ExcelPainter_latest.py qplogo_invert_lg.png temp.chain tests/Example Data (A).cache +*.css +*.ttf +*.js +*.eot +*.svg +*.woff From 1ea42fa619b54adb55376f80a67a317bfda20f42 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Fri, 3 Feb 2017 12:14:36 +0100 Subject: [PATCH 032/733] replacement for stack.py because :confused_parrot: --- core/stack.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/core/stack.py b/core/stack.py index 46a3dbe4c..badc05c58 100644 --- a/core/stack.py +++ b/core/stack.py @@ -1505,7 +1505,10 @@ def sort_expanded_nets(self, view, within=True, between=True, ascending=False, if (v in net_groups['codes'] or v in net_groups.keys()) and not v in fix_codes] if between: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) + if pd.__version__ == '0.19.2': + temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) + else: + temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) else: temp_df = df.loc[sort] between_order = temp_df.index.get_level_values(1).tolist() @@ -1522,7 +1525,10 @@ def sort_expanded_nets(self, view, within=True, between=True, ascending=False, fixed_net_name = g[0] sort = [(name, v) for v in g[1:]] if within: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) + if pd.__version__ == '0.19.2': + temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) + else: + temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) else: temp_df = df.loc[sort] new_idx = [fixed_net_name] + temp_df.index.get_level_values(1).tolist() @@ -1589,11 +1595,13 @@ def get_rules_slicer_via_stack(self, data_key, the_filter, transposed_array_sum = True except: return None + if not rules: return None views = self[data_key][the_filter][col]['@'].keys() w = '' if weight is None else weight expanded_net = [v for v in views if '}+]' in v and v.split('|')[-2] == w - and not v.split('|')[1].startswith('t.')] + and v.split('|')[1] == 'f' and + not v.split('|')[3] == 'x'] if expanded_net: if len(expanded_net) > 1: if len(expanded_net) == 2: @@ -1604,7 +1612,11 @@ def get_rules_slicer_via_stack(self, data_key, the_filter, raise RuntimeError(msg.format(col)) else: expanded_net = expanded_net[0] - if 'sortx' in rules and rules['sortx'].get('sort_on', '@') == 'mean': + if 'sortx' in rules: + on_mean = rules['sortx'].get('sort_on', '@') == 'mean' + else: + on_mean = False + if 'sortx' in rules and on_mean: f = self.get_descriptive_via_stack( data_key, the_filter, col, weight=weight) elif 'sortx' in rules and expanded_net: @@ -1621,7 +1633,7 @@ def get_rules_slicer_via_stack(self, data_key, the_filter, if transposed_array_sum: rules_slicer = functions.get_rules_slicer(f.T, rules) else: - if not expanded_net: + if not expanded_net or ('sortx' in rules and on_mean): rules_slicer = functions.get_rules_slicer(f, rules) else: rules_slicer = f.index.values.tolist() @@ -1629,4 +1641,4 @@ def get_rules_slicer_via_stack(self, data_key, the_filter, rules_slicer.remove((col, 'All')) except: pass - return rules_slicer \ No newline at end of file + return rules_slicer From 204db7f02efd1ee07fd09c1b878566639f19fdc5 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Fri, 3 Feb 2017 13:01:44 +0100 Subject: [PATCH 033/733] bugfix in DataSet.copy() --- core/dataset.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index 0b4188d7a..887f44297 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2605,12 +2605,7 @@ def copy(self, name, suffix='rec', copy_data=True, slicer=None): mask_meta_copy = org_copy.deepcopy(meta['masks'][name]) if not 'masks@' + copy_name in meta['sets']['data file']['items']: meta['sets']['data file']['items'].append('masks@' + copy_name) - mask_set = [] - for i, i_meta in zip(items, mask_meta_copy['items']): - self.copy(i, suffix, copy_data, slicer) - i_name = '{}_{}'.format(i, suffix) - i_meta['source'] = 'columns@{}'.format(i_name) - mask_set.append('columns@{}'.format(i_name)) + mask_set = [] lib_ref = 'lib@values@{}'.format(copy_name) lib_copy = org_copy.deepcopy(meta['lib']['values'][name]) if 'ddf' in meta['lib']['values'].keys(): @@ -2621,6 +2616,11 @@ def copy(self, name, suffix='rec', copy_data=True, slicer=None): if 'ddf' in meta['lib']['values'].keys(): meta['lib']['values']['ddf'][copy_name] = lib_copy_ddf meta['sets'][copy_name] = {'items': mask_set} + for i, i_meta in zip(items, mask_meta_copy['items']): + self.copy(i, suffix, copy_data, slicer) + i_name = '{}_{}'.format(i, suffix) + i_meta['source'] = 'columns@{}'.format(i_name) + mask_set.append('columns@{}'.format(i_name)) else: if copy_data: if slicer: @@ -2637,7 +2637,7 @@ def copy(self, name, suffix='rec', copy_data=True, slicer=None): if self._is_array_item(name) and self._has_categorical_data(name): ref = '{}_{}'.format(self._maskname_from_item(name), suffix) if ref in meta['lib']['values']: - lib_ref = 'lib@values@{}_{}'.format(ref) + lib_ref = 'lib@values@{}'.format(ref) else: lib_ref = self._get_value_loc(name) meta['columns'][copy_name]['values'] = lib_ref From 88a57343df7a3f98914584348e77866b6f17a547 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Wed, 3 May 2017 11:46:37 +0200 Subject: [PATCH 034/733] bugfix in view_maps --- core/view_generators/view_maps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/view_generators/view_maps.py b/core/view_generators/view_maps.py index 72ff7aba6..9ff95a8bb 100644 --- a/core/view_generators/view_maps.py +++ b/core/view_generators/view_maps.py @@ -122,6 +122,7 @@ def __init_known_methods__(self): 'method': 'frequency', 'kwargs': { 'text': '', + 'axis': 'x', 'condition': 'x++' } } @@ -129,6 +130,7 @@ def __init_known_methods__(self): 'method': 'frequency', 'kwargs': { 'text': '', + 'axis': 'x', 'condition': 'x++', 'rel_to': 'y' } @@ -316,6 +318,7 @@ def frequency(self, link, name, kwargs): else: raw = True if name in ['counts_sum', 'c%_sum'] else False cum_sum = True if name in ['counts_cumsum', 'c%_cumsum'] else False + if cum_sum: axis = None q.count(axis=axis, raw_sum=raw, as_df=False, margin=False, cum_sum=cum_sum) if rel_to is not None: if q.type == 'array': From 275e4526f6c4ca1cf443669265c72c72b8db2cca Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Wed, 3 May 2017 13:32:49 +0200 Subject: [PATCH 035/733] bugfix for wrong has_calc() inspection --- core/quantify/engine.py | 1 + core/view.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/quantify/engine.py b/core/quantify/engine.py index 8b6bc9e2f..a2b1667c9 100644 --- a/core/quantify/engine.py +++ b/core/quantify/engine.py @@ -860,6 +860,7 @@ def count(self, axis=None, raw_sum=False, cum_sum=False, margin=True, as_df=True np.cumsum(counts[1:, :], axis=0, out=counts[1:, :]) # updating margins! if self.rbase is not None: self.rbase = counts[:, [0]] + if self.cbase is not None: self.cbase = counts[[0], :] self.result = counts elif axis == 'x': if raw_sum: diff --git a/core/view.py b/core/view.py index 69b829f4b..20a4cf9da 100644 --- a/core/view.py +++ b/core/view.py @@ -532,7 +532,7 @@ def has_other_source(self): return False def has_calc(self): - return 'f.c' in self._notation.split('|')[1] + return 'f.c' in self._notation.split('|')[1] and not self.is_cumulative() def is_cumulative(self): From 66980ad14a5dab9571935998d90688a2d7514025 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Tue, 23 May 2017 15:53:51 +0200 Subject: [PATCH 036/733] add tag for dimensions comp to meta['info'] --- core/dataset.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index 3b8b4fc5d..c29a8a920 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -516,7 +516,9 @@ def read_quantipy(self, path_meta, path_data): print msg.format(self._get_type(col), renamed) self.rename(col, renamed) self.undimensionize() - if self._dimensions_comp: self.dimensionize() + if self._dimensions_comp: + self.dimensionize() + self._meta['info']['dimensions_comp'] = True return None def read_dimensions(self, path_meta, path_data): @@ -544,7 +546,9 @@ def read_dimensions(self, path_meta, path_data): self._meta, self._data = r_dimensions(path_meta+'.mdd', path_data+'.ddf') self._set_file_info(path_data, path_meta) self.undimensionize() - if self._dimensions_comp: self.dimensionize() + if self._dimensions_comp: + self.dimensionize() + self._meta['info']['dimensions_comp'] = True return None def read_ascribe(self, path_meta, path_data, text_key): @@ -727,6 +731,10 @@ def from_components(self, data_df, meta_dict=None, text_key=None): self.text_key = None self.set_verbose_infomsg(False) self._set_file_info('') + if self._meta['info'].get('dimensions_comp'): + self._dimensions_comp = True + else: + self._dimensions_comp = False return None def from_stack(self, stack, datakey=None, dk_filter=None): From e38aac41b6e2936ee9adfaf61f8d258d9721174d Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 8 Jun 2017 10:08:24 +0200 Subject: [PATCH 037/733] fix merge conflict --- core/dataset.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index dc8fec2f8..cc5c64da3 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -694,11 +694,7 @@ def read_quantipy(self, path_meta, path_data): print msg.format(self._get_type(col), renamed) self.rename(col, renamed) self.undimensionize() -<<<<<<< HEAD - if self._dimensions_comp: -======= if self._dimensions_comp: ->>>>>>> i757-DataSet_fixes_and_features_part13 self.dimensionize() self._meta['info']['dimensions_comp'] = True return None @@ -728,11 +724,7 @@ def read_dimensions(self, path_meta, path_data): self._meta, self._data = r_dimensions(path_meta+'.mdd', path_data+'.ddf') self._set_file_info(path_data, path_meta) self.undimensionize() -<<<<<<< HEAD - if self._dimensions_comp: -======= if self._dimensions_comp: ->>>>>>> i757-DataSet_fixes_and_features_part13 self.dimensionize() self._meta['info']['dimensions_comp'] = True return None From 96ecc61198c61b2e6ee5683eeaff903e9fea361c Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Thu, 22 Jun 2017 08:54:56 +0200 Subject: [PATCH 038/733] accidently did not save in prior commit --- core/stack.py | 1709 ------------------------------------------------- 1 file changed, 1709 deletions(-) diff --git a/core/stack.py b/core/stack.py index 8d5f56406..9fcd9cde6 100644 --- a/core/stack.py +++ b/core/stack.py @@ -1,1712 +1,3 @@ -#-*- coding: utf-8 -*- -import io -import itertools -import json -import pandas as pd -import numpy as np -import quantipy as qp -import copy - -from link import Link -from chain import Chain -from view import View -from helpers import functions -from view_generators.view_mapper import ViewMapper -from view_generators.view_maps import QuantipyViews -from quantipy.core.tools.dp.spss.reader import parse_sav_file -from quantipy.core.tools.dp.io import unicoder, write_quantipy -from quantipy.core.tools.dp.prep import frequency, verify_test_results -from cache import Cache - -import itertools -from collections import defaultdict, OrderedDict - -# Pickle modules -import cPickle - -# Compression methods -import gzip - -class Stack(defaultdict): - """ - Container of quantipy.Link objects holding View objects. - - A Stack is nested dictionary that structures the data and variable - relationships storing all View aggregations performed. - """ - - def __init__(self, - name="", - add_data=None): - - super(Stack, self).__init__(Stack) - - self.name = name - self.key = None - self.parent = None - - # This is the root of the stack - # It is used by the get/set methods to determine - # WHERE in the stack those methods are. - self.stack_pos = "stack_root" - - self.x_variables = None - self.y_variables = None - - self.__view_keys = [] - - if add_data: - for key in add_data: - if isinstance(add_data[key], dict): - self.add_data( - data_key=key, - data=add_data[key].get('data', None), - meta=add_data[key].get('meta', None) - ) - elif isinstance(add_data[key], tuple): - self.add_data( - data_key=key, - data=add_data[key][0], - meta=add_data[key][1] - ) - else: - raise TypeError( - "All data_key values must be one of the following types: " - " or . " - "Given: %s" % (type(add_data[key])) - ) - - def __setstate__(self, attr_dict): - self.__dict__.update(attr_dict) - - def __reduce__(self): - arguments = (self.name, ) - state = self.__dict__.copy() - if 'cache' in state: - state.pop('cache') - state['cache'] = Cache() # Empty the cache for storage - return self.__class__, arguments, state, None, self.iteritems() - - def __setitem__(self, key, val): - """ The 'set' method for the Stack(dict) - - It 'sets' the value in it's correct place in the Stack - AND applies a 'stack_pos' value depending on WHERE in - the stack the value is being placed. - """ - super(Stack, self).__setitem__(key, val) - - # The 'meta' portion of the stack is a standar dict (not Stack) - try: - if isinstance(val, Stack) and val.stack_pos is "stack_root": - val.parent = self - val.key = key - - # This needs to be compacted and simplified. - if self.stack_pos is "stack_root": - val.stack_pos = "data_root" - elif self.stack_pos is "data_root": - val.stack_pos = "filter" - elif self.stack_pos is "filter": - val.stack_pos = "x" - - except AttributeError: - pass - - def __getitem__(self, key): - """ The 'get' method for the Stack(dict) - - The method 'gets' a value from the stack. If 'stack_pos' is 'y' - AND the value isn't a Link instance THEN it tries to query the - stack again with the x/y variables swapped and IF that yelds - a result that is a Link object THEN it sets a 'transpose' variable - as True in the result and the result is transposed. - """ - val = defaultdict.__getitem__(self, key) - return val - - def add_data(self, data_key, data=None, meta=None, ): - """ - Sets the data_key into the stack, optionally mapping data sources it. - - It is possible to handle the mapping of data sources in different ways: - - * no meta or data (for proxy links not connected to source data) - * meta only (for proxy links with supporintg meta) - * data only (meta will be inferred if possible) - * data and meta - - Parameters - ---------- - data_key : str - The reference name for a data source connected to the Stack. - data : pandas.DataFrame - The input (case) data source. - meta : dict or OrderedDict - A quantipy compatible metadata source that describes the case data. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_key) - - if data_key in self.keys(): - warning_msg = "You have overwritten data/meta for key: ['%s']." - print warning_msg % (data_key) - - if data is not None: - if isinstance(data, pd.DataFrame): - if meta is None: - # To do: infer meta from DataFrame - meta = {'info': None, 'lib': None, 'sets': None, - 'columns': None, 'masks': None} - # Add a special column of 1s - data['@1'] = np.ones(len(data.index)) - data.index = list(xrange(0, len(data.index))) - else: - raise TypeError( - "The 'data' given to Stack.add_data() must be one of the following types: " - "" - ) - - if not meta is None: - if isinstance(meta, (dict, OrderedDict)): - # To do: verify incoming meta - pass - else: - raise TypeError( - "The 'meta' given to Stack.add_data() must be one of the following types: " - ", ." - ) - - # Add the data key to the stack - # self[data_key] = {} - - # Add the meta and data to the data_key position in the stack - self[data_key].meta = meta - self[data_key].data = data - self[data_key].cache = Cache() - self[data_key]['no_filter'].data = self[data_key].data - - def remove_data(self, data_keys): - """ - Deletes the data_key(s) and associated data specified in the Stack. - - Parameters - ---------- - data_keys : str or list of str - The data keys to remove. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_keys) - if isinstance(data_keys, (str, unicode)): - data_keys = [data_keys] - for data_key in data_keys: - del self[data_key] - - def variable_types(self, data_key, only_type=None): - """ - Group variables by data types found in the meta. - - Parameters - ---------- - data_key : str - The reference name of a case data source hold by the Stack instance. - only_type : {'int', 'float', 'single', 'delimited set', 'string', - 'date', time', 'array'}, optional - Will restrict the output to the given data type. - - Returns - ------- - types : dict or list of str - A summary of variable names mapped to their data types, in form of - {type_name: [variable names]} or a list of variable names - confirming only_type. - """ - if self[data_key].meta['columns'] is None: - return 'No meta attached to data_key: %s' %(data_key) - else: - types = { - 'int': [], - 'float': [], - 'single': [], - 'delimited set': [], - 'string': [], - 'date': [], - 'time': [], - 'array': [] - } - not_found = [] - for col in self[data_key].data.columns: - if not col in ['@1', 'id_L1', 'id_L1.1']: - try: - types[ - self[data_key].meta['columns'][col]['type'] - ].append(col) - except: - not_found.append(col) - for mask in self[data_key].meta['masks'].keys(): - types[self[data_key].meta['masks'][mask]['type']].append(mask) - if not_found: - print '%s not found in meta file. Ignored.' %(not_found) - if only_type: - return types[only_type] - else: - return types - - - def get_chain(self, name=None, data_keys=None, filters=None, x=None, y=None, - views=None, orient_on=None, select=None, - rules=False, rules_weight=None): - """ - Construct a "chain" shaped subset of Links and their Views from the Stack. - - A chain is a one-to-one or one-to-many relation with an orientation that - defines from which axis (x or y) it is build. - - Parameters - ---------- - name : str, optional - If not provided the name of the chain is generated automatically. - data_keys, filters, x, y, views : str or list of str - Views will be added reflecting the order in ``views`` parameter. If - both ``x`` and ``y`` have multiple items, you must specify the - ``orient_on`` parameter. - orient_on : {'x', 'y'}, optional - Must be specified if both ``x`` and ``y`` are lists of multiple - items. - select : tbc. - :TODO: document this! - - Returns - ------- - chain : Chain object instance - """ - - #Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - # filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if orient_on is None: - if len(x)==1: - orientation = 'x' - elif len(y)==1: - orientation = 'y' - else: - orientation = 'x' - else: - orientation = orient_on - - described = self.describe() - - if isinstance(rules, bool): - if rules: - rules = ['x', 'y'] - else: - rules = [] - - if orient_on: - if x is None: - x = described['x'].drop_duplicates().values.tolist() - if y is None: - y = described['y'].drop_duplicates().values.tolist() - if views is None: - views = self._Stack__view_keys - views = [v for v in views if '|default|' not in v] - chains = self.__get_chains( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y, - views=views, - orientation=orient_on, - select=select, - rules=rules, - rules_weight=rules_weight) - return chains - else: - chain = Chain(name) - found_views = [] - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if data_keys is None: - # Apply lazy data_keys if none given - data_keys = self.keys() - - the_filter = "no_filter" if filters is None else filters - - if self.__has_list(data_keys): - for key in data_keys: - - # Use describe method to get x keys if not supplied. - if x is None: - x_keys = described['x'].drop_duplicates().values.tolist() - else: - x_keys = x - - # Use describe method to get y keys if not supplied. - if y is None: - y_keys = described['y'].drop_duplicates().values.tolist() - else: - y_keys = y - - # Use describe method to get view keys if not supplied. - if views is None: - v_keys = described['view'].drop_duplicates().values.tolist() - v_keys = [v_key for v_key in v_keys if '|default|' - not in v_key] - else: - v_keys = views - - chain._derive_attributes( - key, the_filter, x_keys, y_keys, views, orientation=orientation) - - # Apply lazy name if none given - if name is None: - chain._lazy_name() - - for x_key in x_keys: - self._verify_key_exists( - x_key, - stack_path=[key, the_filter] - ) - - for y_key in y_keys: - self._verify_key_exists( - y_key, - stack_path=[key, the_filter, x_key]) - - - try: - base_text = self[key].meta['columns'][x_key]['properties']['base_text'] - if isinstance(base_text, (str, unicode)): - if base_text.startswith(('Base:', 'Bas:')): - base_text = base_text.split(':')[-1].lstrip() - elif isinstance(base_text, dict): - for text_key in base_text.keys(): - if base_text[text_key].startswith(('Base:', 'Bas:')): - base_text[text_key] = base_text[text_key].split(':')[-1].lstrip() - chain.base_text = base_text - except: - pass - if views is None: - chain[key][the_filter][x_key][y_key] = self[key][the_filter][x_key][y_key] - else: - stack_link = self[key][the_filter][x_key][y_key] - link_keys = stack_link.keys() - chain_link = {} - chain_view_keys = [k for k in views if k in link_keys] - for vk in chain_view_keys: - stack_view = stack_link[vk] - # Get view dataframe - rules_x_slicer = self.axis_slicer_from_vartype( - rules, 'x', key, the_filter, x_key, y_key, rules_weight) - - rules_y_slicer = self.axis_slicer_from_vartype( - rules, 'y', key, the_filter, x_key, y_key, rules_weight) - if rules_x_slicer is None and rules_y_slicer is None: - # No rules to apply - view_df = stack_view.dataframe - else: - # Apply rules - viable_axes = functions.rule_viable_axes(self[key].meta, vk, x_key, y_key) - transposed_array_sum = x_key == '@' and y_key in self[key].meta['masks'] - if not viable_axes: - # Axes are not viable for rules application - view_df = stack_view.dataframe - else: - view_df = stack_view.dataframe.copy() - if 'x' in viable_axes and not rules_x_slicer is None: - # Apply x-rules - rule_codes = set(rules_x_slicer) - view_codes = set(view_df.index.tolist()) - if not rule_codes - view_codes: - view_df = view_df.loc[rules_x_slicer] - if 'x' in viable_axes and transposed_array_sum and rules_y_slicer: - view_df = view_df.loc[rules_y_slicer] - if 'y' in viable_axes and not rules_y_slicer is None: - # Apply y-rules - view_df = view_df[rules_y_slicer] - if vk.split('|')[1].startswith('t.'): - view_df = verify_test_results(view_df) - chain_view = View( - link=stack_link, - name = stack_view.name, - kwargs=stack_view._kwargs) - chain_view._notation = vk - chain_view.grp_text_map = stack_view.grp_text_map - chain_view.dataframe = view_df - chain_view._custom_txt = stack_view._custom_txt - chain_view.add_base_text = stack_view.add_base_text - chain_link[vk] = chain_view - if vk not in found_views: - found_views.append(vk) - - chain[key][the_filter][x_key][y_key] = chain_link - else: - raise ValueError( - "One or more of your data_keys ({data_keys}) is not" - " in the stack ({stack_keys})".format( - data_keys=data_keys, - stack_keys=self.keys() - ) - ) - - # Make sure chain.views only contains views that actually exist - # in the chain - if found_views: - chain.views = [ - view - for view in chain.views - if view in found_views] - - return chain - - - - - - def reduce(self, data_keys=None, filters=None, x=None, y=None, variables=None, views=None): - ''' - Remove keys from the matching levels, erasing discrete Stack portions. - - Parameters - ---------- - data_keys, filters, x, y, views : str or list of str - - Returns - ------- - None - ''' - - # Ensure given keys are all valid types - self._verify_multiple_key_types( - data_keys=data_keys, - filters=filters, - x=x, - y=y, - variables=variables, - views=views - ) - - # Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - if not variables is None: - variables = self._force_key_as_list(variables) - x = variables - y = variables - else: - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Make sure no keys that don't exist anywhere were passed - key_check = { - 'data': data_keys, - 'filter': filters, - 'x': x, - 'y': y, - 'view': views - } - - contents = self.describe() - for key_type, keys in key_check.iteritems(): - if not keys is None: - uk = contents[key_type].unique() - if not any([tk in uk for tk in keys]): - raise ValueError( - "Some of the %s keys passed to stack.reduce() " - "weren't found. Found: %s. " - "Given: %s" % (key_type, uk, keys) - ) - - if not data_keys is None: - for dk in data_keys: - try: - del self[dk] - except: - pass - - for dk in self.keys(): - if not filters is None: - for fk in filters: - try: - del self[dk][fk] - except: - pass - - for fk in self[dk].keys(): - if not x is None: - for xk in x: - try: - del self[dk][fk][xk] - except: - pass - - for xk in self[dk][fk].keys(): - if not y is None: - for yk in y: - try: - del self[dk][fk][xk][yk] - except: - pass - - for yk in self[dk][fk][xk].keys(): - if not views is None: - for vk in views: - try: - del self[dk][fk][xk][yk][vk] - except: - pass - - def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, - views=None, weights=None, variables=None): - """ - Add Link and View defintions to the Stack. - - The method can be used flexibly: It is possible to pass only Link - defintions that might be composed of filter, x and y specifications, - only views incl. weight variable selections or arbitrary combinations of - the former. - - :TODO: Remove ``variables`` from parameter list and method calls. - - Parameters - ---------- - data_keys : str, optional - The data_key to be added to. If none is given, the method will try - to add to all data_keys found in the Stack. - filters : list of str describing filter defintions, default ['no_filter'] - The string must be a valid input for the - pandas.DataFrame.query() method. - x, y : str or list of str - The x and y variables to constrcut Links from. - views : list of view method names. - Can be any of Quantipy's preset Views or the names of created - view method specifications. - weights : list, optional - The names of weight variables to consider in the data aggregation - process. Weight variables must be of type ``float``. - - Returns - ------- - None - """ - if data_keys is None: - data_keys = self.keys() - else: - self._verify_key_types(name='data', keys=data_keys) - data_keys = self._force_key_as_list(data_keys) - - if not isinstance(views, ViewMapper): - # Use DefaultViews if no view were given - if views is None: - pass - # views = DefaultViews() - elif isinstance(views, (list, tuple)): - views = QuantipyViews(views=views) - else: - raise TypeError( - "The views past to stack.add_link() must be type , " - "or they must be a list of method names known to ." - ) - - qplogic_filter = False - if not isinstance(filters, dict): - self._verify_key_types(name='filter', keys=filters) - filters = self._force_key_as_list(filters) - filters = {f: f for f in filters} - # if filters.keys()[0] != 'no_filter': - # msg = ("Warning: pandas-based filtering will be deprecated in the " - # "future!\nPlease switch to quantipy-logic expressions.") - # print UserWarning(msg) - else: - qplogic_filter = True - - if not variables is None: - if not x is None or not y is None: - raise ValueError( - "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " - "at the same time." - ) - - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Get the lazy y keys none were given and there is only 1 x key - if not x is None: - if len(x)==1 and y is None: - y = self.describe( - index=['y'], - query="x=='%s'" % (x[0]) - ).index.tolist() - - # Get the lazy x keys none were given and there is only 1 y key - if not y is None: - if len(y)==1 and x is None: - x = self.describe( - index=['x'], - query="y=='%s'" % (y[0]) - ).index.tolist() - - for dk in data_keys: - self._verify_key_exists(dk) - for filter_def, logic in filters.items(): - # if not filter_def in self[dk].keys(): - if filter_def=='no_filter': - self[dk][filter_def].data = self[dk].data - self[dk][filter_def].meta = self[dk].meta - else: - if not qplogic_filter: - try: - self[dk][filter_def].data = self[dk].data.query(logic) - self[dk][filter_def].meta = self[dk].meta - except Exception, ex: - raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - else: - dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) - f_dataset = dataset.filter(filter_def, logic, inplace=False) - self[dk][filter_def].data = f_dataset._data - self[dk][filter_def].meta = f_dataset._meta - - fdata = self[dk][filter_def].data - if len(fdata) == 0: - raise UserWarning('A filter definition resulted in no cases and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - self.__create_links(data=fdata, data_key=dk, the_filter=filter_def, x=x, y=y, views=views, weights=weights, variables=variables) - - def describe(self, index=None, columns=None, query=None, split_view_names=False): - """ - Generates a structured overview of all Link defining Stack elements. - - Parameters - ---------- - index, columns : str of or list of {'data', 'filter', 'x', 'y', 'view'}, - optional - Controls the output representation by structuring a pivot-style - table according to the index and column values. - query : str - A query string that is valid for the pandas.DataFrame.query() method. - split_view_names : bool, default False - If True, will create an output of unique view name notations split - up into their components. - - Returns - ------- - description : pandas.DataFrame - DataFrame summing the Stack's structure in terms of Links and Views. - """ - stack_tree = [] - for dk in self.keys(): - path_dk = [dk] - filters = self[dk] - -# for fk in filters.keys(): -# path_fk = path_dk + [fk] -# xs = self[dk][fk] - - for fk in filters.keys(): - path_fk = path_dk + [fk] - xs = self[dk][fk] - - for sk in xs.keys(): - path_sk = path_fk + [sk] - ys = self[dk][fk][sk] - - for tk in ys.keys(): - path_tk = path_sk + [tk] - views = self[dk][fk][sk][tk] - - if views.keys(): - for vk in views.keys(): - path_vk = path_tk + [vk, 1] - stack_tree.append(tuple(path_vk)) - else: - path_vk = path_tk + ['|||||', 1] - stack_tree.append(tuple(path_vk)) - - column_names = ['data', 'filter', 'x', 'y', 'view', '#'] - description = pd.DataFrame.from_records(stack_tree, columns=column_names) - if split_view_names: - views_as_series = pd.DataFrame( - description.pivot_table(values='#', columns='view', aggfunc='count') - ).reset_index()['view'] - parts = ['xpos', 'agg', 'condition', 'rel_to', 'weights', - 'shortname'] - description = pd.concat( - (views_as_series, - pd.DataFrame(views_as_series.str.split('|').tolist(), - columns=parts)), axis=1) - - description.replace('|||||', np.NaN, inplace=True) - if query is not None: - description = description.query(query) - if not index is None or not columns is None: - description = description.pivot_table(values='#', index=index, columns=columns, - aggfunc='count') - return description - - def refresh(self, data_key, new_data_key='', new_weight=None, - new_data=None, new_meta=None): - """ - Re-run all or a portion of Stack's aggregations for a given data key. - - refresh() can be used to re-weight the data using a new case data - weight variable or to re-run all aggregations based on a changed source - data version (e.g. after cleaning the file/ dropping cases) or a - combination of the both. - - .. note:: - Currently this is only supported for the preset QuantipyViews(), - namely: ``'cbase'``, ``'rbase'``, ``'counts'``, ``'c%'``, - ``'r%'``, ``'mean'``, ``'ebase'``. - - Parameters - ---------- - data_key : str - The Links' data key to be modified. - new_data_key : str, default '' - Controls if the existing data key's files and aggregations will be - overwritten or stored via a new data key. - new_weight : str - The name of a new weight variable used to re-aggregate the Links. - new_data : pandas.DataFrame - The case data source. If None is given, the - original case data found for the data key will be used. - new_meta : quantipy meta document - A meta data source associated with the case data. If None is given, - the original meta definition found for the data key will be used. - - Returns - ------- - None - """ - content = self.describe()[['data', 'filter', 'x', 'y', 'view']] - content = content[content['data'] == data_key] - put_meta = self[data_key].meta if new_meta is None else new_meta - put_data = self[data_key].data if new_data is None else new_data - dk = new_data_key if new_data_key else data_key - self.add_data(data_key=dk, data=put_data, meta=put_meta) - skipped_views = [] - for _, f, x, y, view in content.values: - shortname = view.split('|')[-1] - if shortname not in ['default', 'cbase', 'cbase_gross', - 'rbase', 'counts', 'c%', - 'r%', 'ebase', 'mean', - 'c%_sum', 'counts_sum']: - if view not in skipped_views: - skipped_views.append(view) - warning_msg = ('\nOnly preset QuantipyViews are supported.' - 'Skipping: {}').format(view) - print warning_msg - else: - view_weight = view.split('|')[-2] - if not x in [view_weight, new_weight]: - if new_data is None and new_weight is not None: - if not view_weight == '': - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - if new_weight == '': - weight = None - else: - weight = [None, new_weight] - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - else: - if view_weight == '': - weight = None - elif new_weight is not None: - if not (view_weight == new_weight): - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - weight = view_weight - else: - weight = view_weight - try: - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - except ValueError, e: - print '\n', e - return None - - def save(self, path_stack, compression="gzip", store_cache=True, - decode_str=False, dataset=False, describe=False): - """ - Save Stack instance to .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The intended compression type. - store_cache : bool, default True - Stores the MatrixCache in a file in the same location. - decode_str : bool, default=True - If True the unicoder function will be used to decode all str - objects found anywhere in the meta document/s. - dataset : bool, default=False - If True a json/csv will be saved parallel to the saved stack - for each data key in the stack. - describe : bool, default=False - If True the result of stack.describe().to_excel() will be - saved parallel to the saved stack. - - Returns - ------- - None - """ - protocol = cPickle.HIGHEST_PROTOCOL - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.save() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.save(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.save(path_stack='%s', ...)" % (path_stack) - ) - - # Make sure there are no str objects in any meta documents. If - # there are any non-ASCII characters will be encoded - # incorrectly and lead to UnicodeDecodeErrors in Jupyter. - if decode_str: - for dk in self.keys(): - self[dk].meta = unicoder(self[dk].meta) - - if compression is None: - f = open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - else: - f = gzip.open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - - if store_cache: - caches = {} - for key in self.keys(): - caches[key] = self[key].cache - - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f1 = open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - else: - f1 = gzip.open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - - f1.close() - - f.close() - - if dataset: - for key in self.keys(): - path_json = path_stack.replace( - '.stack', - ' [{}].json'.format(key)) - path_csv = path_stack.replace( - '.stack', - ' [{}].csv'.format(key)) - write_quantipy( - meta=self[key].meta, - data=self[key].data, - path_json=path_json, - path_csv=path_csv) - - if describe: - path_describe = path_stack.replace('.stack', '.xlsx') - self.describe().to_excel(path_describe) - - # def get_slice(data_key=None, x=None, y=None, filters=None, views=None): - # """ """ - # pass - - # STATIC METHODS - - @staticmethod - def from_sav(data_key, filename, name=None, path=None, ioLocale="en_US.UTF-8", ioUtf8=True): - """ - Creates a new stack instance from a .sav file. - - Parameters - ---------- - data_key : str - The data_key for the data and meta in the sav file. - filename : str - The name to the sav file. - name : str - A name for the sav (stored in the meta). - path : str - The path to the sav file. - ioLocale : str - The locale used in during the sav processing. - ioUtf8 : bool - Boolean that indicates the mode in which text communicated to or - from the I/O module will be. - - Returns - ------- - stack : stack object instance - A stack instance that has a data_key with data and metadata - to run aggregations. - """ - if name is None: - name = data_key - - meta, data = parse_sav_file(filename=filename, path=path, name=name, ioLocale=ioLocale, ioUtf8=ioUtf8) - return Stack(add_data={name: {'meta': meta, 'data':data}}) - - @staticmethod - def load(path_stack, compression="gzip", load_cache=False): - """ - Load Stack instance from .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The compression type that has been used saving the file. - load_cache : bool, default False - Loads MatrixCache into the Stack a .cache file is found. - - Returns - ------- - None - """ - - - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.load() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.load(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.load(path_stack='%s', ...)" % (path_stack) - ) - - if compression is None: - f = open(path_stack, 'rb') - else: - f = gzip.open(path_stack, 'rb') - new_stack = cPickle.load(f) - f.close() - - if load_cache: - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f = open(path_cache, 'rb') - else: - f = gzip.open(path_cache, 'rb') - caches = cPickle.load(f) - for key in caches.keys(): - if key in new_stack.keys(): - new_stack[key].cache = caches[key] - else: - raise ValueError( - "Tried to insert a loaded MatrixCache in to a data_key in the stack that" - "is not in the stack. The data_key is '{}', available keys are {}" - .format(key, caches.keys()) - ) - f.close() - - return new_stack - - - # PRIVATE METHODS - - def __get_all_y_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].y_variables - else: - raise KeyError("get_all_y_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].x_variables - else: - raise KeyError("get_all_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys_except(self, data_key, exception): - keys = self.__get_all_x_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __get_all_y_keys_except(self, data_key, exception): - keys = self.__get_all_y_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __set_x_key(self, key): - if self.x_variables is None: - self.x_variables = set(key) - else: - self.x_variables.update(key) - - def __set_y_key(self, key): - if self.y_variables is None: - self.y_variables = set(key) - else: - self.y_variables.update(key) - - def _set_x_and_y_keys(self, data_key, x, y): - """ - Sets the x_variables and y_variables in the data part of the stack for this data_key, e.g. stack['Jan']. - This method can also be used to add to the current lists and it makes sure the list stays unique. - """ - if self.stack_pos == 'stack_root': - self[data_key].__set_x_key(x) - self[data_key].__set_y_key(y) - else: - raise KeyError("set_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __create_combinations(self, data, data_key, x=None, y=None, weight=None, variables=None): - if isinstance(y, str): - y = [y] - if isinstance(x, str): - x = [x] - - has_metadata = self[data_key].meta is not None and not isinstance(self[data_key].meta, Stack) - - # any(...) returns true if ANY of the vars are not None - if any([x, y]) and variables is not None: - # Raise an error if variables AND x/y are BOTH supplied - raise ValueError("Either use the 'variables' OR 'x', 'y' NOT both.") - - if not any([x, y]): - if variables is None: - if not has_metadata: - # "fully-lazy" method. (variables, x and y are all None) - variables = data.columns.tolist() - - if variables is not None: - x = variables - y = variables - variables = None - - # Ensure that we actually have metadata - if has_metadata: - # THEN we try to create the combinations with metadata - combinations = self.__create_combinations_with_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - else: - # Either variables or both x AND y are supplied. Then create the combinations from that. - combinations = self.__create_combinations_no_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - - unique_list = set([item for comb in combinations for item in comb]) - - return combinations, unique_list - - def __create_combinations_with_meta(self, data, data_key, x=None, y=None, weight=None): - # TODO: These meta functions should possibly be in the helpers functions - metadata_columns = self[data_key].meta['columns'].keys() - for mask, mask_data in self[data_key].meta['masks'].iteritems(): - # TODO :: Get the static list from somewhere. not hardcoded. - if mask_data['type'].lower() in ['array', 'dichotomous set', - "categorical set"]: - metadata_columns.append(mask) - for item in mask_data['items']: - if "source" in item: - column = item["source"].split('@')[1] - metadata_columns.remove(column) - elif mask_data['type'].lower() in ["overlay"]: - pass - # Use all from the metadata, if nothing is specified (fully-lazy) - if x is None and y is None: - x = metadata_columns - y = metadata_columns - if all([x, y]): - metadata_columns = list(set(metadata_columns + x + y)) - elif x is not None: - metadata_columns = list(set(metadata_columns + x)) - elif y is not None: - metadata_columns = list(set(metadata_columns + y)) - combinations = functions.create_combinations_from_array(sorted(metadata_columns)) - - for var in [x, y]: - if var is not None: - if weight in var: - var.remove(weight) - if all([x, y]): - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x and y_item in y] - elif x is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x] - elif y is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if y_item in y] - - return combinations - - def __create_combinations_no_meta(self, data, data_key, x=None, y=None, weight=None): - if x is None: - x = data.columns.tolist() - if y is None: - y = data.columns.tolist() - for var in [x, y]: - if weight in var: - var.remove(weight) - combinations = [(x_item, y_item) for x_item in x for y_item - in y if x_item != y_item] - self._set_x_and_y_keys(data_key, x, y) - - return combinations - - def __create_links(self, data, data_key, views, variables=None, x=None, y=None, - the_filter=None, store_view_in_link=False, weights=None): - if views is not None: - has_links = True if self[data_key][the_filter].keys() else False - if has_links: - xs = self[data_key][the_filter].keys() - if x is not None: - valid_x = [xk for xk in xs if xk in x] - valid_x.extend(x) - x = set(valid_x) - else: - x = xs - ys = list(set(itertools.chain.from_iterable( - [self[data_key][the_filter][xk].keys() - for xk in xs]))) - if y is not None: - valid_y = [yk for yk in ys if yk in y] - valid_y.extend(y) - y = set(valid_y) - else: - y = ys - if self._x_and_y_keys_in_file(data_key, data, x, y): - for x_key, y_key in itertools.product(x, y): - if x_key==y_key and x_key=='@': - continue - if y_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y='@', - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key]['@'] = link - else: - link = self[data_key][the_filter][x_key]['@'] - elif x_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x='@', - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter]['@'][y_key] = link - else: - link = self[data_key][the_filter]['@'][y_key] - else: - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key][y_key] = link - else: - link = self[data_key][the_filter][x_key][y_key] - if views is not None: - views._apply_to(link, weights) - - def _x_and_y_keys_in_file(self, data_key, data, x, y): - data_columns = data.columns.tolist() - if '>' in ','.join(y): y = self._clean_from_nests(y) - if '>' in ','.join(x): - raise NotImplementedError('x-axis Nesting not supported.') - x_not_found = [var for var in x if not var in data_columns - and not var == '@'] - y_not_found = [var for var in y if not var in data_columns - and not var == '@'] - if x_not_found is not None: - masks_meta_lookup_x = [var for var in x_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_x: - x_not_found.remove(found_in_meta) - if y_not_found is not None: - masks_meta_lookup_y = [var for var in y_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_y: - y_not_found.remove(found_in_meta) - if not x_not_found and not y_not_found: - return True - elif x_not_found and y_not_found: - raise ValueError( - 'data key {}: x: {} and y: {} not found.'.format( - data_key, x_not_found, y_not_found)) - elif x_not_found: - raise ValueError( - 'data key {}: x: {} not found.'.format( - data_key, x_not_found)) - elif y_not_found: - raise ValueError( - 'data key {}: y: {} not found.'.format( - data_key, y_not_found)) - - def _clean_from_nests(self, variables): - cleaned = [] - nests = [var for var in variables if '>' in var] - non_nests = [var for var in variables if not '>' in var] - for nest in nests: - cleaned.extend([var.strip() for var in nest.split('>')]) - non_nests += cleaned - non_nests = list(set(non_nests)) - return non_nests - - def __clean_column_names(self, columns): - """ - Remove extra doublequotes if there are any - """ - cols = [] - for column in columns: - cols.append(column.replace('"', '')) - return cols - - def __generate_key_from_list_of(self, list_of_keys): - """ - Generate keys from a list (or tuple). - """ - list_of_keys = list(list_of_keys) - list_of_keys.sort() - return ",".join(list_of_keys) - - def __has_list(self, small): - """ - Check if object contains a list of strings. - """ - keys = self.keys() - for i in xrange(len(keys)-len(small)+1): - for j in xrange(len(small)): - if keys[i+j] != small[j]: - break - else: - return i, i+len(small) - return False - - def __get_all_combinations(self, list_of_items): - """Generates all combinations of items from a list """ - return [itertools.combinations(list_of_items, index+1) - for index in range(len(list_of_items))] - - def __get_stack_pointer(self, stack_pos): - """Takes a stack_pos and returns the stack with that location - raises an exception IF the stack pointer is not found - """ - if self.parent.stack_pos == stack_pos: - return self.parent - else: - return self.parent.__get_stack_pointer(stack_pos) - - def __get_chains(self, name, data_keys, filters, x, y, views, - orientation, select, rules, - rules_weight): - """ - List comprehension wrapper around .get_chain(). - """ - if orientation == 'y': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y_var, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for y_var in y - ] - elif orientation == 'x': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x_var, - y=y, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for x_var in x - ] - else: - raise ValueError( - "Unknown orientation type. Please use 'x' or 'y'." - ) - - def _verify_multiple_key_types(self, data_keys=None, filters=None, x=None, - y=None, variables=None, views=None): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if data_keys is not None: - self._verify_key_types(name='data', keys=data_keys) - - if filters is not None: - self._verify_key_types(name='filter', keys=filters) - - if x is not None: - self._verify_key_types(name='x', keys=x) - - if y is not None: - self._verify_key_types(name='y', keys=y) - - if variables is not None: - self._verify_key_types(name='variables', keys=variables) - - if views is not None: - self._verify_key_types(name='view', keys=views) - - def _verify_key_exists(self, key, stack_path=[]): - """ - Verify that the given key exists in the stack at the path targeted. - """ - error_msg = ( - "Could not find the {key_type} key '{key}' in: {stack_path}. " - "Found {keys_found} instead." - ) - try: - dk = stack_path[0] - fk = stack_path[1] - xk = stack_path[2] - yk = stack_path[3] - vk = stack_path[4] - except: - pass - try: - if len(stack_path) == 0: - if key not in self: - key_type, keys_found = 'data', self.keys() - stack_path = 'stack' - raise ValueError - elif len(stack_path) == 1: - if key not in self[dk]: - key_type, keys_found = 'filter', self[dk].keys() - stack_path = "stack['{dk}']".format( - dk=dk) - raise ValueError - elif len(stack_path) == 2: - if key not in self[dk][fk]: - key_type, keys_found = 'x', self[dk][fk].keys() - stack_path = "stack['{dk}']['{fk}']".format( - dk=dk, fk=fk) - raise ValueError - elif len(stack_path) == 3: - if key not in self[dk][fk][xk]: - key_type, keys_found = 'y', self[dk][fk][xk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']".format( - dk=dk, fk=fk, xk=xk) - raise ValueError - elif len(stack_path) == 4: - if key not in self[dk][fk][xk][yk]: - key_type, keys_found = 'view', self[dk][fk][xk][yk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']['{yk}']".format( - dk=dk, fk=fk, xk=xk, yk=yk) - raise ValueError - except ValueError: - print error_msg.format( - key_type=key_type, - key=key, - stack_path=stack_path, - keys_found=keys_found - ) - - def _force_key_as_list(self, key): - """Returns key as [key] if it is str or unicode""" - return [key] if isinstance(key, (str, unicode)) else key - - def _verify_key_types(self, name, keys): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if isinstance(keys, (list, tuple)): - for key in keys: - self._verify_key_types(name, key) - elif isinstance(keys, (str, unicode)): - pass - else: - raise TypeError( - "All %s keys must be one of the following types: " - " or , " - " of or , " - " of or . " - "Given: %s" % (name, keys) - ) - - def _find_groups(self, view): - groups = OrderedDict() - logic = view._kwargs.get('logic') - description = view.describe_block() - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - net_names = [v for v, d in description.items() if d == 'net'] - for l in logic: - new_l = copy.deepcopy(l) - for k in l: - if k not in net_names: - del new_l[k] - groups[new_l.keys()[0]] = new_l.values()[0] - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - return groups - - def sort_expanded_nets(self, view, within=True, between=True, ascending=False, - fix=None): - if not within and not between: - return view.dataframe - df = view.dataframe - name = df.index.levels[0][0] - if not fix: - fix_codes = [] - else: - if not isinstance(fix, list): - fix_codes = [fix] - else: - fix_codes = fix - fix_codes = [c for c in fix_codes if c in - df.index.get_level_values(1).tolist()] - net_groups = self._find_groups(view) - sort_col = (df.columns.levels[0][0], '@') - sort = [(name, v) for v in df.index.get_level_values(1) - if (v in net_groups['codes'] or - v in net_groups.keys()) and not v in fix_codes] - if between: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - between_order = temp_df.index.get_level_values(1).tolist() - code_group_list = [] - for g in between_order: - if g in net_groups: - code_group_list.append([g] + net_groups[g]) - elif g in net_groups['codes']: - code_group_list.append([g]) - final_index = [] - for g in code_group_list: - is_code = len(g) == 1 - if not is_code: - fixed_net_name = g[0] - sort = [(name, v) for v in g[1:]] - if within: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - new_idx = [fixed_net_name] + temp_df.index.get_level_values(1).tolist() - final_index.extend(new_idx) - else: - final_index.extend(g) - final_index = [(name, i) for i in final_index] - if fix_codes: - fix_codes = [(name, f) for f in fix_codes] - final_index.extend(fix_codes) - df = df.reindex(final_index) - return df - - def get_frequency_via_stack(self, data_key, the_filter, col, weight=None): - weight_notation = '' if weight is None else weight - vk = 'x|f|:||{}|counts'.format(weight_notation) - try: - f = self[data_key][the_filter][col]['@'][vk].dataframe - except (KeyError, AttributeError) as e: - try: - f = self[data_key][the_filter]['@'][col][vk].dataframe.T - except (KeyError, AttributeError) as e: - f = frequency(self[data_key].meta, self[data_key].data, x=col, weight=weight) - return f - - def get_descriptive_via_stack(self, data_key, the_filter, col, weight=None): - l = self[data_key][the_filter][col]['@'] - w = '' if weight is None else weight - mean_key = [k for k in l.keys() if 'd.mean' in k.split('|')[1] and - k.split('|')[-2] == w] - if not mean_key: - msg = "No mean view to sort '{}' on found!" - raise RuntimeError(msg.format(col)) - elif len(mean_key) > 1: - msg = "Multiple mean views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - mean_key = mean_key[0] - vk = mean_key - d = l[mean_key].dataframe - return d - - def _is_array_summary(self, meta, x, y): - return x in meta['masks'] - - def _is_transposed_summary(self, meta, x, y): - return x == '@' and y in meta['masks'] - - def axis_slicer_from_vartype(self, all_rules_axes, rules_axis, dk, the_filter, x, y, rules_weight): - if rules_axis == 'x' and 'x' not in all_rules_axes: - return None - elif rules_axis == 'y' and 'y' not in all_rules_axes: - return None - meta = self[dk].meta - - array_summary = self._is_array_summary(meta, x, y) - transposed_summary = self._is_transposed_summary(meta, x, y) - - axis_slicer = None - - if rules_axis == 'x': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=True) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - elif rules_axis == 'y': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, y=y, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=False) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - - return axis_slicer - - def get_rules_slicer_via_stack(self, data_key, the_filter, - x=None, y=None, weight=None, - slice_array_items=False): - m = self[data_key].meta - array_summary = self._is_array_summary(m, x, y) - transposed_summary = self._is_transposed_summary(m, x, y) - - rules = None - - if not array_summary and not transposed_summary: - if not x is None: - try: - rules = self[data_key].meta['columns'][x]['rules']['x'] - col = x - except: - pass - elif not y is None: - try: - rules = self[data_key].meta['columns'][y]['rules']['y'] - col = y - except: - pass - - elif array_summary: - if slice_array_items: - try: - rules = self[data_key].meta['masks'][x]['rules']['x'] - col = x - except: - pass - else: - try: - rules = self[data_key].meta['masks'][x]['rules']['y'] - col = x - except: - pass - - elif transposed_summary: - try: - rules = self[data_key].meta['masks'][y]['rules']['x'] - col = y - except: - pass - - if not rules: return None - views = self[data_key][the_filter][col]['@'].keys() - w = '' if weight is None else weight - expanded_net = [v for v in views if '}+]' in v - and v.split('|')[-2] == w - and v.split('|')[1] == 'f' and - not v.split('|')[3] == 'x'] - if expanded_net: - if len(expanded_net) > 1: - if len(expanded_net) == 2: - if expanded_net[0].split('|')[2] == expanded_net[1].split('|')[2]: - expanded_net = expanded_net[0] - else: - msg = "Multiple 'expand' using views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - expanded_net = expanded_net[0] - if 'sortx' in rules: - on_mean = rules['sortx'].get('sort_on', '@') == 'mean' - else: - on_mean = False - if 'sortx' in rules and on_mean: - f = self.get_descriptive_via_stack( - data_key, the_filter, col, weight=weight) - elif 'sortx' in rules and expanded_net: - within = rules['sortx'].get('within', False) - between = rules['sortx'].get('between', False) - fix = rules['sortx'].get('fixed', False) - ascending = rules['sortx'].get('ascending', False) - view = self[data_key][the_filter][col]['@'][expanded_net] - f = self.sort_expanded_nets(view, between=between, within=within, - ascending=ascending, fix=fix) - else: - f = self.get_frequency_via_stack( - data_key, the_filter, col, weight=weight) - - if transposed_summary or (not slice_array_items and array_summary): - rules_slicer = functions.get_rules_slicer(f.T, rules) - else: - if not expanded_net or ('sortx' in rules and on_mean): - rules_slicer = functions.get_rules_slicer(f, rules) - else: - rules_slicer = f.index.values.tolist() - try: - rules_slicer.remove((col, 'All')) - except: - pass - return rules_slicer -======= - #-*- coding: utf-8 -*- import io import itertools From dffe24f8fcd73209790bf19e442432953df48d25 Mon Sep 17 00:00:00 2001 From: Alexander Tanski Date: Wed, 12 Jul 2017 12:34:29 +0200 Subject: [PATCH 039/733] undo: print + ignore if DataSet is dimensions_comp. --- core/dataset.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/dataset.py b/core/dataset.py index 187994a52..03eb42a96 100644 --- a/core/dataset.py +++ b/core/dataset.py @@ -2219,12 +2219,8 @@ def dimensionize(self, names=None): """ Rename the dataset columns for Dimensions compatibility. """ - if self._dimensions_comp: - msg = "DataSet is already in Dimensions compatibility mode." - print msg - else: - mapper = self.dimensionizing_mapper(names) - self.rename_from_mapper(mapper) + mapper = self.dimensionizing_mapper(names) + self.rename_from_mapper(mapper) @modify(to_list='names') @verify(variables={'names': 'both'}) From b2e2196d91ffd69b2e8b468ea6f9eb354c48c668 Mon Sep 17 00:00:00 2001 From: "YG\\alasdaire" Date: Wed, 26 Jul 2017 10:28:57 +0100 Subject: [PATCH 040/733] Actually resolving conflict --- core/stack.py | 2291 ------------------------------------------------- 1 file changed, 2291 deletions(-) diff --git a/core/stack.py b/core/stack.py index 2b48fc736..e3b6c7419 100644 --- a/core/stack.py +++ b/core/stack.py @@ -2289,2294 +2289,3 @@ def _remove_coltests(self, props=True, means=True): if del_prop or del_mean: del self[dk][fk][xk][yk][vk] return None -======= - -#-*- coding: utf-8 -*- -import io -import itertools -import json -import pandas as pd -import numpy as np -import quantipy as qp -import copy -import time -import sys - -from link import Link -from chain import Chain -from view import View -from helpers import functions -from view_generators.view_mapper import ViewMapper -from view_generators.view_maps import QuantipyViews -from quantipy.core.tools.qp_decorators import modify -from quantipy.core.tools.dp.spss.reader import parse_sav_file -from quantipy.core.tools.dp.io import unicoder, write_quantipy -from quantipy.core.tools.dp.prep import frequency, verify_test_results -from cache import Cache - -import itertools -from collections import defaultdict, OrderedDict - -# Pickle modules -import cPickle - -# Compression methods -import gzip - -from quantipy.sandbox.sandbox import Chain as NewChain - - -class Stack(defaultdict): - """ - Container of quantipy.Link objects holding View objects. - - A Stack is nested dictionary that structures the data and variable - relationships storing all View aggregations performed. - """ - - def __init__(self, - name="", - add_data=None): - - super(Stack, self).__init__(Stack) - - self.name = name - self.key = None - self.parent = None - - # This is the root of the stack - # It is used by the get/set methods to determine - # WHERE in the stack those methods are. - self.stack_pos = "stack_root" - - self.x_variables = None - self.y_variables = None - - self.__view_keys = [] - - if add_data: - for key in add_data: - if isinstance(add_data[key], dict): - self.add_data( - data_key=key, - data=add_data[key].get('data', None), - meta=add_data[key].get('meta', None) - ) - elif isinstance(add_data[key], tuple): - self.add_data( - data_key=key, - data=add_data[key][0], - meta=add_data[key][1] - ) - else: - raise TypeError( - "All data_key values must be one of the following types: " - " or . " - "Given: %s" % (type(add_data[key])) - ) - - def __setstate__(self, attr_dict): - self.__dict__.update(attr_dict) - - def __reduce__(self): - arguments = (self.name, ) - state = self.__dict__.copy() - if 'cache' in state: - state.pop('cache') - state['cache'] = Cache() # Empty the cache for storage - return self.__class__, arguments, state, None, self.iteritems() - - def __setitem__(self, key, val): - """ The 'set' method for the Stack(dict) - - It 'sets' the value in it's correct place in the Stack - AND applies a 'stack_pos' value depending on WHERE in - the stack the value is being placed. - """ - super(Stack, self).__setitem__(key, val) - - # The 'meta' portion of the stack is a standar dict (not Stack) - try: - if isinstance(val, Stack) and val.stack_pos is "stack_root": - val.parent = self - val.key = key - - # This needs to be compacted and simplified. - if self.stack_pos is "stack_root": - val.stack_pos = "data_root" - elif self.stack_pos is "data_root": - val.stack_pos = "filter" - elif self.stack_pos is "filter": - val.stack_pos = "x" - - except AttributeError: - pass - - def __getitem__(self, key): - """ The 'get' method for the Stack(dict) - - The method 'gets' a value from the stack. If 'stack_pos' is 'y' - AND the value isn't a Link instance THEN it tries to query the - stack again with the x/y variables swapped and IF that yelds - a result that is a Link object THEN it sets a 'transpose' variable - as True in the result and the result is transposed. - """ - val = defaultdict.__getitem__(self, key) - return val - - def add_data(self, data_key, data=None, meta=None, ): - """ - Sets the data_key into the stack, optionally mapping data sources it. - - It is possible to handle the mapping of data sources in different ways: - - * no meta or data (for proxy links not connected to source data) - * meta only (for proxy links with supporintg meta) - * data only (meta will be inferred if possible) - * data and meta - - Parameters - ---------- - data_key : str - The reference name for a data source connected to the Stack. - data : pandas.DataFrame - The input (case) data source. - meta : dict or OrderedDict - A quantipy compatible metadata source that describes the case data. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_key) - - if data_key in self.keys(): - warning_msg = "You have overwritten data/meta for key: ['%s']." - print warning_msg % (data_key) - - if data is not None: - if isinstance(data, pd.DataFrame): - if meta is None: - # To do: infer meta from DataFrame - meta = {'info': None, 'lib': None, 'sets': None, - 'columns': None, 'masks': None} - # Add a special column of 1s - data['@1'] = np.ones(len(data.index)) - data.index = list(xrange(0, len(data.index))) - else: - raise TypeError( - "The 'data' given to Stack.add_data() must be one of the following types: " - "" - ) - - if not meta is None: - if isinstance(meta, (dict, OrderedDict)): - # To do: verify incoming meta - pass - else: - raise TypeError( - "The 'meta' given to Stack.add_data() must be one of the following types: " - ", ." - ) - - # Add the data key to the stack - # self[data_key] = {} - - # Add the meta and data to the data_key position in the stack - self[data_key].meta = meta - self[data_key].data = data - self[data_key].cache = Cache() - self[data_key]['no_filter'].data = self[data_key].data - - def remove_data(self, data_keys): - """ - Deletes the data_key(s) and associated data specified in the Stack. - - Parameters - ---------- - data_keys : str or list of str - The data keys to remove. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_keys) - if isinstance(data_keys, (str, unicode)): - data_keys = [data_keys] - for data_key in data_keys: - del self[data_key] - - def variable_types(self, data_key, only_type=None, verbose=True): - """ - Group variables by data types found in the meta. - - Parameters - ---------- - data_key : str - The reference name of a case data source hold by the Stack instance. - only_type : {'int', 'float', 'single', 'delimited set', 'string', - 'date', time', 'array'}, optional - Will restrict the output to the given data type. - - Returns - ------- - types : dict or list of str - A summary of variable names mapped to their data types, in form of - {type_name: [variable names]} or a list of variable names - confirming only_type. - """ - if self[data_key].meta['columns'] is None: - return 'No meta attached to data_key: %s' %(data_key) - else: - types = { - 'int': [], - 'float': [], - 'single': [], - 'delimited set': [], - 'string': [], - 'date': [], - 'time': [], - 'array': [] - } - not_found = [] - for col in self[data_key].data.columns: - if not col in ['@1', 'id_L1', 'id_L1.1']: - try: - types[ - self[data_key].meta['columns'][col]['type'] - ].append(col) - except: - not_found.append(col) - for mask in self[data_key].meta['masks'].keys(): - types[self[data_key].meta['masks'][mask]['type']].append(mask) - if not_found and verbose: - print '%s not found in meta file. Ignored.' %(not_found) - if only_type: - return types[only_type] - else: - return types - - def get_chain(self, *args, **kwargs): - - if qp.OPTIONS['new_chains']: - chain = NewChain(self, name=None) - chain = chain.get(*args, **kwargs) - return chain - else: - def _get_chain(name=None, data_keys=None, filters=None, x=None, y=None, - views=None, orient_on=None, select=None, - rules=False, rules_weight=None): - """ - Construct a "chain" shaped subset of Links and their Views from the Stack. - - A chain is a one-to-one or one-to-many relation with an orientation that - defines from which axis (x or y) it is build. - - Parameters - ---------- - name : str, optional - If not provided the name of the chain is generated automatically. - data_keys, filters, x, y, views : str or list of str - Views will be added reflecting the order in ``views`` parameter. If - both ``x`` and ``y`` have multiple items, you must specify the - ``orient_on`` parameter. - orient_on : {'x', 'y'}, optional - Must be specified if both ``x`` and ``y`` are lists of multiple - items. - select : tbc. - :TODO: document this! - - Returns - ------- - chain : Chain object instance - """ - - #Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - # filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if orient_on is None: - if len(x)==1: - orientation = 'x' - elif len(y)==1: - orientation = 'y' - else: - orientation = 'x' - else: - orientation = orient_on - - described = self.describe() - - if isinstance(rules, bool): - if rules: - rules = ['x', 'y'] - else: - rules = [] - - if orient_on: - if x is None: - x = described['x'].drop_duplicates().values.tolist() - if y is None: - y = described['y'].drop_duplicates().values.tolist() - if views is None: - views = self._Stack__view_keys - views = [v for v in views if '|default|' not in v] - chains = self.__get_chains( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y, - views=views, - orientation=orient_on, - select=select, - rules=rules, - rules_weight=rules_weight) - return chains - else: - chain = Chain(name) - found_views = [] - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if data_keys is None: - # Apply lazy data_keys if none given - data_keys = self.keys() - - the_filter = "no_filter" if filters is None else filters - - if self.__has_list(data_keys): - for key in data_keys: - - # Use describe method to get x keys if not supplied. - if x is None: - x_keys = described['x'].drop_duplicates().values.tolist() - else: - x_keys = x - - # Use describe method to get y keys if not supplied. - if y is None: - y_keys = described['y'].drop_duplicates().values.tolist() - else: - y_keys = y - - # Use describe method to get view keys if not supplied. - if views is None: - v_keys = described['view'].drop_duplicates().values.tolist() - v_keys = [v_key for v_key in v_keys if '|default|' - not in v_key] - else: - v_keys = views - - chain._derive_attributes( - key, the_filter, x_keys, y_keys, views, orientation=orientation) - - # Apply lazy name if none given - if name is None: - chain._lazy_name() - - for x_key in x_keys: - self._verify_key_exists( - x_key, - stack_path=[key, the_filter] - ) - - for y_key in y_keys: - self._verify_key_exists( - y_key, - stack_path=[key, the_filter, x_key]) - - - try: - base_text = self[key].meta['columns'][x_key]['properties']['base_text'] - if isinstance(base_text, (str, unicode)): - if base_text.startswith(('Base:', 'Bas:')): - base_text = base_text.split(':')[-1].lstrip() - elif isinstance(base_text, dict): - for text_key in base_text.keys(): - if base_text[text_key].startswith(('Base:', 'Bas:')): - base_text[text_key] = base_text[text_key].split(':')[-1].lstrip() - chain.base_text = base_text - except: - pass - if views is None: - chain[key][the_filter][x_key][y_key] = self[key][the_filter][x_key][y_key] - else: - stack_link = self[key][the_filter][x_key][y_key] - link_keys = stack_link.keys() - chain_link = {} - chain_view_keys = [k for k in views if k in link_keys] - for vk in chain_view_keys: - stack_view = stack_link[vk] - # Get view dataframe - rules_x_slicer = self.axis_slicer_from_vartype( - rules, 'x', key, the_filter, x_key, y_key, rules_weight) - - rules_y_slicer = self.axis_slicer_from_vartype( - rules, 'y', key, the_filter, x_key, y_key, rules_weight) - if rules_x_slicer is None and rules_y_slicer is None: - # No rules to apply - view_df = stack_view.dataframe - else: - # Apply rules - viable_axes = functions.rule_viable_axes(self[key].meta, vk, x_key, y_key) - transposed_array_sum = x_key == '@' and y_key in self[key].meta['masks'] - if not viable_axes: - # Axes are not viable for rules application - view_df = stack_view.dataframe - else: - view_df = stack_view.dataframe.copy() - if 'x' in viable_axes and not rules_x_slicer is None: - # Apply x-rules - rule_codes = set(rules_x_slicer) - view_codes = set(view_df.index.tolist()) - if not rule_codes - view_codes: - view_df = view_df.loc[rules_x_slicer] - if 'x' in viable_axes and transposed_array_sum and rules_y_slicer: - view_df = view_df.loc[rules_y_slicer] - if 'y' in viable_axes and not rules_y_slicer is None: - # Apply y-rules - view_df = view_df[rules_y_slicer] - if vk.split('|')[1].startswith('t.'): - view_df = verify_test_results(view_df) - chain_view = View( - link=stack_link, - name = stack_view.name, - kwargs=stack_view._kwargs) - chain_view._notation = vk - chain_view.grp_text_map = stack_view.grp_text_map - chain_view.dataframe = view_df - chain_view._custom_txt = stack_view._custom_txt - chain_view.add_base_text = stack_view.add_base_text - chain_link[vk] = chain_view - if vk not in found_views: - found_views.append(vk) - - chain[key][the_filter][x_key][y_key] = chain_link - else: - raise ValueError( - "One or more of your data_keys ({data_keys}) is not" - " in the stack ({stack_keys})".format( - data_keys=data_keys, - stack_keys=self.keys() - ) - ) - - # Make sure chain.views only contains views that actually exist - # in the chain - if found_views: - chain.views = [ - view - for view in chain.views - if view in found_views] - return chain - - return _get_chain(*args, **kwargs) - - - - - - - def reduce(self, data_keys=None, filters=None, x=None, y=None, variables=None, views=None): - ''' - Remove keys from the matching levels, erasing discrete Stack portions. - - Parameters - ---------- - data_keys, filters, x, y, views : str or list of str - - Returns - ------- - None - ''' - - # Ensure given keys are all valid types - self._verify_multiple_key_types( - data_keys=data_keys, - filters=filters, - x=x, - y=y, - variables=variables, - views=views - ) - - # Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - if not variables is None: - variables = self._force_key_as_list(variables) - x = variables - y = variables - else: - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Make sure no keys that don't exist anywhere were passed - key_check = { - 'data': data_keys, - 'filter': filters, - 'x': x, - 'y': y, - 'view': views - } - - contents = self.describe() - for key_type, keys in key_check.iteritems(): - if not keys is None: - uk = contents[key_type].unique() - if not any([tk in uk for tk in keys]): - raise ValueError( - "Some of the %s keys passed to stack.reduce() " - "weren't found. Found: %s. " - "Given: %s" % (key_type, uk, keys) - ) - - if not data_keys is None: - for dk in data_keys: - try: - del self[dk] - except: - pass - - for dk in self.keys(): - if not filters is None: - for fk in filters: - try: - del self[dk][fk] - except: - pass - - for fk in self[dk].keys(): - if not x is None: - for xk in x: - try: - del self[dk][fk][xk] - except: - pass - - for xk in self[dk][fk].keys(): - if not y is None: - for yk in y: - try: - del self[dk][fk][xk][yk] - except: - pass - - for yk in self[dk][fk][xk].keys(): - if not views is None: - for vk in views: - try: - del self[dk][fk][xk][yk][vk] - except: - pass - - def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, - views=None, weights=None, variables=None): - """ - Add Link and View defintions to the Stack. - - The method can be used flexibly: It is possible to pass only Link - defintions that might be composed of filter, x and y specifications, - only views incl. weight variable selections or arbitrary combinations of - the former. - - :TODO: Remove ``variables`` from parameter list and method calls. - - Parameters - ---------- - data_keys : str, optional - The data_key to be added to. If none is given, the method will try - to add to all data_keys found in the Stack. - filters : list of str describing filter defintions, default ['no_filter'] - The string must be a valid input for the - pandas.DataFrame.query() method. - x, y : str or list of str - The x and y variables to constrcut Links from. - views : list of view method names. - Can be any of Quantipy's preset Views or the names of created - view method specifications. - weights : list, optional - The names of weight variables to consider in the data aggregation - process. Weight variables must be of type ``float``. - - Returns - ------- - None - """ - if data_keys is None: - data_keys = self.keys() - else: - self._verify_key_types(name='data', keys=data_keys) - data_keys = self._force_key_as_list(data_keys) - - if not isinstance(views, ViewMapper): - # Use DefaultViews if no view were given - if views is None: - pass - # views = DefaultViews() - elif isinstance(views, (list, tuple)): - views = QuantipyViews(views=views) - else: - raise TypeError( - "The views past to stack.add_link() must be type , " - "or they must be a list of method names known to ." - ) - - qplogic_filter = False - if not isinstance(filters, dict): - self._verify_key_types(name='filter', keys=filters) - filters = self._force_key_as_list(filters) - filters = {f: f for f in filters} - # if filters.keys()[0] != 'no_filter': - # msg = ("Warning: pandas-based filtering will be deprecated in the " - # "future!\nPlease switch to quantipy-logic expressions.") - # print UserWarning(msg) - else: - qplogic_filter = True - - if not variables is None: - if not x is None or not y is None: - raise ValueError( - "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " - "at the same time." - ) - - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Get the lazy y keys none were given and there is only 1 x key - if not x is None: - if len(x)==1 and y is None: - y = self.describe( - index=['y'], - query="x=='%s'" % (x[0]) - ).index.tolist() - - # Get the lazy x keys none were given and there is only 1 y key - if not y is None: - if len(y)==1 and x is None: - x = self.describe( - index=['x'], - query="y=='%s'" % (y[0]) - ).index.tolist() - - for dk in data_keys: - self._verify_key_exists(dk) - for filter_def, logic in filters.items(): - # if not filter_def in self[dk].keys(): - if filter_def=='no_filter': - self[dk][filter_def].data = self[dk].data - self[dk][filter_def].meta = self[dk].meta - else: - if not qplogic_filter: - try: - self[dk][filter_def].data = self[dk].data.query(logic) - self[dk][filter_def].meta = self[dk].meta - except Exception, ex: - raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - else: - dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) - f_dataset = dataset.filter(filter_def, logic, inplace=False) - self[dk][filter_def].data = f_dataset._data - self[dk][filter_def].meta = f_dataset._meta - - fdata = self[dk][filter_def].data - if len(fdata) == 0: - raise UserWarning('A filter definition resulted in no cases and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - self.__create_links(data=fdata, data_key=dk, the_filter=filter_def, x=x, y=y, views=views, weights=weights, variables=variables) - - def describe(self, index=None, columns=None, query=None, split_view_names=False): - """ - Generates a structured overview of all Link defining Stack elements. - - Parameters - ---------- - index, columns : str of or list of {'data', 'filter', 'x', 'y', 'view'}, - optional - Controls the output representation by structuring a pivot-style - table according to the index and column values. - query : str - A query string that is valid for the pandas.DataFrame.query() method. - split_view_names : bool, default False - If True, will create an output of unique view name notations split - up into their components. - - Returns - ------- - description : pandas.DataFrame - DataFrame summing the Stack's structure in terms of Links and Views. - """ - stack_tree = [] - for dk in self.keys(): - path_dk = [dk] - filters = self[dk] - -# for fk in filters.keys(): -# path_fk = path_dk + [fk] -# xs = self[dk][fk] - - for fk in filters.keys(): - path_fk = path_dk + [fk] - xs = self[dk][fk] - - for sk in xs.keys(): - path_sk = path_fk + [sk] - ys = self[dk][fk][sk] - - for tk in ys.keys(): - path_tk = path_sk + [tk] - views = self[dk][fk][sk][tk] - - if views.keys(): - for vk in views.keys(): - path_vk = path_tk + [vk, 1] - stack_tree.append(tuple(path_vk)) - else: - path_vk = path_tk + ['|||||', 1] - stack_tree.append(tuple(path_vk)) - - column_names = ['data', 'filter', 'x', 'y', 'view', '#'] - description = pd.DataFrame.from_records(stack_tree, columns=column_names) - if split_view_names: - views_as_series = pd.DataFrame( - description.pivot_table(values='#', columns='view', aggfunc='count') - ).reset_index()['view'] - parts = ['xpos', 'agg', 'condition', 'rel_to', 'weights', - 'shortname'] - description = pd.concat( - (views_as_series, - pd.DataFrame(views_as_series.str.split('|').tolist(), - columns=parts)), axis=1) - - description.replace('|||||', np.NaN, inplace=True) - if query is not None: - description = description.query(query) - if not index is None or not columns is None: - description = description.pivot_table(values='#', index=index, columns=columns, - aggfunc='count') - return description - - def refresh(self, data_key, new_data_key='', new_weight=None, - new_data=None, new_meta=None): - """ - Re-run all or a portion of Stack's aggregations for a given data key. - - refresh() can be used to re-weight the data using a new case data - weight variable or to re-run all aggregations based on a changed source - data version (e.g. after cleaning the file/ dropping cases) or a - combination of the both. - - .. note:: - Currently this is only supported for the preset QuantipyViews(), - namely: ``'cbase'``, ``'rbase'``, ``'counts'``, ``'c%'``, - ``'r%'``, ``'mean'``, ``'ebase'``. - - Parameters - ---------- - data_key : str - The Links' data key to be modified. - new_data_key : str, default '' - Controls if the existing data key's files and aggregations will be - overwritten or stored via a new data key. - new_weight : str - The name of a new weight variable used to re-aggregate the Links. - new_data : pandas.DataFrame - The case data source. If None is given, the - original case data found for the data key will be used. - new_meta : quantipy meta document - A meta data source associated with the case data. If None is given, - the original meta definition found for the data key will be used. - - Returns - ------- - None - """ - content = self.describe()[['data', 'filter', 'x', 'y', 'view']] - content = content[content['data'] == data_key] - put_meta = self[data_key].meta if new_meta is None else new_meta - put_data = self[data_key].data if new_data is None else new_data - dk = new_data_key if new_data_key else data_key - self.add_data(data_key=dk, data=put_data, meta=put_meta) - skipped_views = [] - for _, f, x, y, view in content.values: - shortname = view.split('|')[-1] - if shortname not in ['default', 'cbase', 'cbase_gross', - 'rbase', 'counts', 'c%', - 'r%', 'ebase', 'mean', - 'c%_sum', 'counts_sum']: - if view not in skipped_views: - skipped_views.append(view) - warning_msg = ('\nOnly preset QuantipyViews are supported.' - 'Skipping: {}').format(view) - print warning_msg - else: - view_weight = view.split('|')[-2] - if not x in [view_weight, new_weight]: - if new_data is None and new_weight is not None: - if not view_weight == '': - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - if new_weight == '': - weight = None - else: - weight = [None, new_weight] - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - else: - if view_weight == '': - weight = None - elif new_weight is not None: - if not (view_weight == new_weight): - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - weight = view_weight - else: - weight = view_weight - try: - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - except ValueError, e: - print '\n', e - return None - - def save(self, path_stack, compression="gzip", store_cache=True, - decode_str=False, dataset=False, describe=False): - """ - Save Stack instance to .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The intended compression type. - store_cache : bool, default True - Stores the MatrixCache in a file in the same location. - decode_str : bool, default=True - If True the unicoder function will be used to decode all str - objects found anywhere in the meta document/s. - dataset : bool, default=False - If True a json/csv will be saved parallel to the saved stack - for each data key in the stack. - describe : bool, default=False - If True the result of stack.describe().to_excel() will be - saved parallel to the saved stack. - - Returns - ------- - None - """ - protocol = cPickle.HIGHEST_PROTOCOL - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.save() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.save(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.save(path_stack='%s', ...)" % (path_stack) - ) - - # Make sure there are no str objects in any meta documents. If - # there are any non-ASCII characters will be encoded - # incorrectly and lead to UnicodeDecodeErrors in Jupyter. - if decode_str: - for dk in self.keys(): - self[dk].meta = unicoder(self[dk].meta) - - if compression is None: - f = open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - else: - f = gzip.open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - - if store_cache: - caches = {} - for key in self.keys(): - caches[key] = self[key].cache - - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f1 = open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - else: - f1 = gzip.open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - - f1.close() - - f.close() - - if dataset: - for key in self.keys(): - path_json = path_stack.replace( - '.stack', - ' [{}].json'.format(key)) - path_csv = path_stack.replace( - '.stack', - ' [{}].csv'.format(key)) - write_quantipy( - meta=self[key].meta, - data=self[key].data, - path_json=path_json, - path_csv=path_csv) - - if describe: - path_describe = path_stack.replace('.stack', '.xlsx') - self.describe().to_excel(path_describe) - - # def get_slice(data_key=None, x=None, y=None, filters=None, views=None): - # """ """ - # pass - - # STATIC METHODS - - @staticmethod - def from_sav(data_key, filename, name=None, path=None, ioLocale="en_US.UTF-8", ioUtf8=True): - """ - Creates a new stack instance from a .sav file. - - Parameters - ---------- - data_key : str - The data_key for the data and meta in the sav file. - filename : str - The name to the sav file. - name : str - A name for the sav (stored in the meta). - path : str - The path to the sav file. - ioLocale : str - The locale used in during the sav processing. - ioUtf8 : bool - Boolean that indicates the mode in which text communicated to or - from the I/O module will be. - - Returns - ------- - stack : stack object instance - A stack instance that has a data_key with data and metadata - to run aggregations. - """ - if name is None: - name = data_key - - meta, data = parse_sav_file(filename=filename, path=path, name=name, ioLocale=ioLocale, ioUtf8=ioUtf8) - return Stack(add_data={name: {'meta': meta, 'data':data}}) - - @staticmethod - def load(path_stack, compression="gzip", load_cache=False): - """ - Load Stack instance from .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The compression type that has been used saving the file. - load_cache : bool, default False - Loads MatrixCache into the Stack a .cache file is found. - - Returns - ------- - None - """ - - - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.load() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.load(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.load(path_stack='%s', ...)" % (path_stack) - ) - - if compression is None: - f = open(path_stack, 'rb') - else: - f = gzip.open(path_stack, 'rb') - new_stack = cPickle.load(f) - f.close() - - if load_cache: - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f = open(path_cache, 'rb') - else: - f = gzip.open(path_cache, 'rb') - caches = cPickle.load(f) - for key in caches.keys(): - if key in new_stack.keys(): - new_stack[key].cache = caches[key] - else: - raise ValueError( - "Tried to insert a loaded MatrixCache in to a data_key in the stack that" - "is not in the stack. The data_key is '{}', available keys are {}" - .format(key, caches.keys()) - ) - f.close() - - return new_stack - - - # PRIVATE METHODS - - def __get_all_y_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].y_variables - else: - raise KeyError("get_all_y_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].x_variables - else: - raise KeyError("get_all_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys_except(self, data_key, exception): - keys = self.__get_all_x_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __get_all_y_keys_except(self, data_key, exception): - keys = self.__get_all_y_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __set_x_key(self, key): - if self.x_variables is None: - self.x_variables = set(key) - else: - self.x_variables.update(key) - - def __set_y_key(self, key): - if self.y_variables is None: - self.y_variables = set(key) - else: - self.y_variables.update(key) - - def _set_x_and_y_keys(self, data_key, x, y): - """ - Sets the x_variables and y_variables in the data part of the stack for this data_key, e.g. stack['Jan']. - This method can also be used to add to the current lists and it makes sure the list stays unique. - """ - if self.stack_pos == 'stack_root': - self[data_key].__set_x_key(x) - self[data_key].__set_y_key(y) - else: - raise KeyError("set_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __create_combinations(self, data, data_key, x=None, y=None, weight=None, variables=None): - if isinstance(y, str): - y = [y] - if isinstance(x, str): - x = [x] - - has_metadata = self[data_key].meta is not None and not isinstance(self[data_key].meta, Stack) - - # any(...) returns true if ANY of the vars are not None - if any([x, y]) and variables is not None: - # Raise an error if variables AND x/y are BOTH supplied - raise ValueError("Either use the 'variables' OR 'x', 'y' NOT both.") - - if not any([x, y]): - if variables is None: - if not has_metadata: - # "fully-lazy" method. (variables, x and y are all None) - variables = data.columns.tolist() - - if variables is not None: - x = variables - y = variables - variables = None - - # Ensure that we actually have metadata - if has_metadata: - # THEN we try to create the combinations with metadata - combinations = self.__create_combinations_with_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - else: - # Either variables or both x AND y are supplied. Then create the combinations from that. - combinations = self.__create_combinations_no_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - - unique_list = set([item for comb in combinations for item in comb]) - - return combinations, unique_list - - def __create_combinations_with_meta(self, data, data_key, x=None, y=None, weight=None): - # TODO: These meta functions should possibly be in the helpers functions - metadata_columns = self[data_key].meta['columns'].keys() - for mask, mask_data in self[data_key].meta['masks'].iteritems(): - # TODO :: Get the static list from somewhere. not hardcoded. - if mask_data['type'].lower() in ['array', 'dichotomous set', - "categorical set"]: - metadata_columns.append(mask) - for item in mask_data['items']: - if "source" in item: - column = item["source"].split('@')[1] - metadata_columns.remove(column) - elif mask_data['type'].lower() in ["overlay"]: - pass - # Use all from the metadata, if nothing is specified (fully-lazy) - if x is None and y is None: - x = metadata_columns - y = metadata_columns - if all([x, y]): - metadata_columns = list(set(metadata_columns + x + y)) - elif x is not None: - metadata_columns = list(set(metadata_columns + x)) - elif y is not None: - metadata_columns = list(set(metadata_columns + y)) - combinations = functions.create_combinations_from_array(sorted(metadata_columns)) - - for var in [x, y]: - if var is not None: - if weight in var: - var.remove(weight) - if all([x, y]): - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x and y_item in y] - elif x is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x] - elif y is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if y_item in y] - - return combinations - - def __create_combinations_no_meta(self, data, data_key, x=None, y=None, weight=None): - if x is None: - x = data.columns.tolist() - if y is None: - y = data.columns.tolist() - for var in [x, y]: - if weight in var: - var.remove(weight) - combinations = [(x_item, y_item) for x_item in x for y_item - in y if x_item != y_item] - self._set_x_and_y_keys(data_key, x, y) - - return combinations - - def __create_links(self, data, data_key, views, variables=None, x=None, y=None, - the_filter=None, store_view_in_link=False, weights=None): - if views is not None: - has_links = True if self[data_key][the_filter].keys() else False - if has_links: - xs = self[data_key][the_filter].keys() - if x is not None: - valid_x = [xk for xk in xs if xk in x] - valid_x.extend(x) - x = set(valid_x) - else: - x = xs - ys = list(set(itertools.chain.from_iterable( - [self[data_key][the_filter][xk].keys() - for xk in xs]))) - if y is not None: - valid_y = [yk for yk in ys if yk in y] - valid_y.extend(y) - y = set(valid_y) - else: - y = ys - if self._x_and_y_keys_in_file(data_key, data, x, y): - for x_key, y_key in itertools.product(x, y): - if x_key==y_key and x_key=='@': - continue - if y_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y='@', - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key]['@'] = link - else: - link = self[data_key][the_filter][x_key]['@'] - elif x_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x='@', - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter]['@'][y_key] = link - else: - link = self[data_key][the_filter]['@'][y_key] - else: - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key][y_key] = link - else: - link = self[data_key][the_filter][x_key][y_key] - if views is not None: - views._apply_to(link, weights) - - def _x_and_y_keys_in_file(self, data_key, data, x, y): - data_columns = data.columns.tolist() - if '>' in ','.join(y): y = self._clean_from_nests(y) - if '>' in ','.join(x): - raise NotImplementedError('x-axis Nesting not supported.') - x_not_found = [var for var in x if not var in data_columns - and not var == '@'] - y_not_found = [var for var in y if not var in data_columns - and not var == '@'] - if x_not_found is not None: - masks_meta_lookup_x = [var for var in x_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_x: - x_not_found.remove(found_in_meta) - if y_not_found is not None: - masks_meta_lookup_y = [var for var in y_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_y: - y_not_found.remove(found_in_meta) - if not x_not_found and not y_not_found: - return True - elif x_not_found and y_not_found: - raise ValueError( - 'data key {}: x: {} and y: {} not found.'.format( - data_key, x_not_found, y_not_found)) - elif x_not_found: - raise ValueError( - 'data key {}: x: {} not found.'.format( - data_key, x_not_found)) - elif y_not_found: - raise ValueError( - 'data key {}: y: {} not found.'.format( - data_key, y_not_found)) - - def _clean_from_nests(self, variables): - cleaned = [] - nests = [var for var in variables if '>' in var] - non_nests = [var for var in variables if not '>' in var] - for nest in nests: - cleaned.extend([var.strip() for var in nest.split('>')]) - non_nests += cleaned - non_nests = list(set(non_nests)) - return non_nests - - def __clean_column_names(self, columns): - """ - Remove extra doublequotes if there are any - """ - cols = [] - for column in columns: - cols.append(column.replace('"', '')) - return cols - - def __generate_key_from_list_of(self, list_of_keys): - """ - Generate keys from a list (or tuple). - """ - list_of_keys = list(list_of_keys) - list_of_keys.sort() - return ",".join(list_of_keys) - - def __has_list(self, small): - """ - Check if object contains a list of strings. - """ - keys = self.keys() - for i in xrange(len(keys)-len(small)+1): - for j in xrange(len(small)): - if keys[i+j] != small[j]: - break - else: - return i, i+len(small) - return False - - def __get_all_combinations(self, list_of_items): - """Generates all combinations of items from a list """ - return [itertools.combinations(list_of_items, index+1) - for index in range(len(list_of_items))] - - def __get_stack_pointer(self, stack_pos): - """Takes a stack_pos and returns the stack with that location - raises an exception IF the stack pointer is not found - """ - if self.parent.stack_pos == stack_pos: - return self.parent - else: - return self.parent.__get_stack_pointer(stack_pos) - - def __get_chains(self, name, data_keys, filters, x, y, views, - orientation, select, rules, - rules_weight): - """ - List comprehension wrapper around .get_chain(). - """ - if orientation == 'y': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y_var, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for y_var in y - ] - elif orientation == 'x': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x_var, - y=y, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for x_var in x - ] - else: - raise ValueError( - "Unknown orientation type. Please use 'x' or 'y'." - ) - - def _verify_multiple_key_types(self, data_keys=None, filters=None, x=None, - y=None, variables=None, views=None): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if data_keys is not None: - self._verify_key_types(name='data', keys=data_keys) - - if filters is not None: - self._verify_key_types(name='filter', keys=filters) - - if x is not None: - self._verify_key_types(name='x', keys=x) - - if y is not None: - self._verify_key_types(name='y', keys=y) - - if variables is not None: - self._verify_key_types(name='variables', keys=variables) - - if views is not None: - self._verify_key_types(name='view', keys=views) - - def _verify_key_exists(self, key, stack_path=[]): - """ - Verify that the given key exists in the stack at the path targeted. - """ - error_msg = ( - "Could not find the {key_type} key '{key}' in: {stack_path}. " - "Found {keys_found} instead." - ) - try: - dk = stack_path[0] - fk = stack_path[1] - xk = stack_path[2] - yk = stack_path[3] - vk = stack_path[4] - except: - pass - try: - if len(stack_path) == 0: - if key not in self: - key_type, keys_found = 'data', self.keys() - stack_path = 'stack' - raise ValueError - elif len(stack_path) == 1: - if key not in self[dk]: - key_type, keys_found = 'filter', self[dk].keys() - stack_path = "stack['{dk}']".format( - dk=dk) - raise ValueError - elif len(stack_path) == 2: - if key not in self[dk][fk]: - key_type, keys_found = 'x', self[dk][fk].keys() - stack_path = "stack['{dk}']['{fk}']".format( - dk=dk, fk=fk) - raise ValueError - elif len(stack_path) == 3: - meta = self[dk].meta - if self._is_array_summary(meta, xk, None) and not key == '@': - pass - elif key not in self[dk][fk][xk]: - key_type, keys_found = 'y', self[dk][fk][xk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']".format( - dk=dk, fk=fk, xk=xk) - raise ValueError - elif len(stack_path) == 4: - if key not in self[dk][fk][xk][yk]: - key_type, keys_found = 'view', self[dk][fk][xk][yk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']['{yk}']".format( - dk=dk, fk=fk, xk=xk, yk=yk) - raise ValueError - except ValueError: - print error_msg.format( - key_type=key_type, - key=key, - stack_path=stack_path, - keys_found=keys_found - ) - - def _force_key_as_list(self, key): - """Returns key as [key] if it is str or unicode""" - return [key] if isinstance(key, (str, unicode)) else key - - def _verify_key_types(self, name, keys): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if isinstance(keys, (list, tuple)): - for key in keys: - self._verify_key_types(name, key) - elif isinstance(keys, (str, unicode)): - pass - else: - raise TypeError( - "All %s keys must be one of the following types: " - " or , " - " of or , " - " of or . " - "Given: %s" % (name, keys) - ) - - def _find_groups(self, view): - groups = OrderedDict() - logic = view._kwargs.get('logic') - description = view.describe_block() - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - net_names = [v for v, d in description.items() if d == 'net'] - for l in logic: - new_l = copy.deepcopy(l) - for k in l: - if k not in net_names: - del new_l[k] - groups[new_l.keys()[0]] = new_l.values()[0] - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - return groups - - def sort_expanded_nets(self, view, within=True, between=True, ascending=False, - fix=None): - if not within and not between: - return view.dataframe - df = view.dataframe - name = df.index.levels[0][0] - if not fix: - fix_codes = [] - else: - if not isinstance(fix, list): - fix_codes = [fix] - else: - fix_codes = fix - fix_codes = [c for c in fix_codes if c in - df.index.get_level_values(1).tolist()] - net_groups = self._find_groups(view) - sort_col = (df.columns.levels[0][0], '@') - sort = [(name, v) for v in df.index.get_level_values(1) - if (v in net_groups['codes'] or - v in net_groups.keys()) and not v in fix_codes] - if between: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - between_order = temp_df.index.get_level_values(1).tolist() - code_group_list = [] - for g in between_order: - if g in net_groups: - code_group_list.append([g] + net_groups[g]) - elif g in net_groups['codes']: - code_group_list.append([g]) - final_index = [] - for g in code_group_list: - is_code = len(g) == 1 - if not is_code: - fixed_net_name = g[0] - sort = [(name, v) for v in g[1:]] - if within: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - new_idx = [fixed_net_name] + temp_df.index.get_level_values(1).tolist() - final_index.extend(new_idx) - else: - final_index.extend(g) - final_index = [(name, i) for i in final_index] - if fix_codes: - fix_codes = [(name, f) for f in fix_codes] - final_index.extend(fix_codes) - df = df.reindex(final_index) - return df - - def get_frequency_via_stack(self, data_key, the_filter, col, weight=None): - weight_notation = '' if weight is None else weight - vk = 'x|f|:||{}|counts'.format(weight_notation) - try: - f = self[data_key][the_filter][col]['@'][vk].dataframe - except (KeyError, AttributeError) as e: - try: - f = self[data_key][the_filter]['@'][col][vk].dataframe.T - except (KeyError, AttributeError) as e: - f = frequency(self[data_key].meta, self[data_key].data, x=col, weight=weight) - return f - - def get_descriptive_via_stack(self, data_key, the_filter, col, weight=None): - l = self[data_key][the_filter][col]['@'] - w = '' if weight is None else weight - mean_key = [k for k in l.keys() if 'd.mean' in k.split('|')[1] and - k.split('|')[-2] == w] - if not mean_key: - msg = "No mean view to sort '{}' on found!" - raise RuntimeError(msg.format(col)) - elif len(mean_key) > 1: - msg = "Multiple mean views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - mean_key = mean_key[0] - vk = mean_key - d = l[mean_key].dataframe - return d - - def _is_array_summary(self, meta, x, y): - return x in meta['masks'] - - def _is_transposed_summary(self, meta, x, y): - return x == '@' and y in meta['masks'] - - def axis_slicer_from_vartype(self, all_rules_axes, rules_axis, dk, the_filter, x, y, rules_weight): - if rules_axis == 'x' and 'x' not in all_rules_axes: - return None - elif rules_axis == 'y' and 'y' not in all_rules_axes: - return None - meta = self[dk].meta - - array_summary = self._is_array_summary(meta, x, y) - transposed_summary = self._is_transposed_summary(meta, x, y) - - axis_slicer = None - - if rules_axis == 'x': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=True) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - elif rules_axis == 'y': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, y=y, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=False) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - - return axis_slicer - - def get_rules_slicer_via_stack(self, data_key, the_filter, - x=None, y=None, weight=None, - slice_array_items=False): - m = self[data_key].meta - array_summary = self._is_array_summary(m, x, y) - transposed_summary = self._is_transposed_summary(m, x, y) - - rules = None - - if not array_summary and not transposed_summary: - if not x is None: - try: - rules = self[data_key].meta['columns'][x]['rules']['x'] - col = x - except: - pass - elif not y is None: - try: - rules = self[data_key].meta['columns'][y]['rules']['y'] - col = y - except: - pass - - elif array_summary: - if slice_array_items: - try: - rules = self[data_key].meta['masks'][x]['rules']['x'] - col = x - except: - pass - else: - try: - rules = self[data_key].meta['masks'][x]['rules']['y'] - col = x - except: - pass - - elif transposed_summary: - try: - rules = self[data_key].meta['masks'][y]['rules']['x'] - col = y - except: - pass - - if not rules: return None - views = self[data_key][the_filter][col]['@'].keys() - w = '' if weight is None else weight - expanded_net = [v for v in views if '}+]' in v - and v.split('|')[-2] == w - and v.split('|')[1] == 'f' and - not v.split('|')[3] == 'x'] - if expanded_net: - if len(expanded_net) > 1: - if len(expanded_net) == 2: - if expanded_net[0].split('|')[2] == expanded_net[1].split('|')[2]: - expanded_net = expanded_net[0] - else: - msg = "Multiple 'expand' using views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - expanded_net = expanded_net[0] - if 'sortx' in rules: - on_mean = rules['sortx'].get('sort_on', '@') == 'mean' - else: - on_mean = False - if 'sortx' in rules and on_mean: - f = self.get_descriptive_via_stack( - data_key, the_filter, col, weight=weight) - elif 'sortx' in rules and expanded_net: - within = rules['sortx'].get('within', False) - between = rules['sortx'].get('between', False) - fix = rules['sortx'].get('fixed', False) - ascending = rules['sortx'].get('ascending', False) - view = self[data_key][the_filter][col]['@'][expanded_net] - f = self.sort_expanded_nets(view, between=between, within=within, - ascending=ascending, fix=fix) - else: - f = self.get_frequency_via_stack( - data_key, the_filter, col, weight=weight) - - if transposed_summary or (not slice_array_items and array_summary): - rules_slicer = functions.get_rules_slicer(f.T, rules) - else: - if not expanded_net or ('sortx' in rules and on_mean): - rules_slicer = functions.get_rules_slicer(f, rules) - else: - rules_slicer = f.index.values.tolist() - try: - rules_slicer.remove((col, 'All')) - except: - pass - return rules_slicer - - @modify(to_list='batches') - def _check_batches(self, dk, batches='all'): - """ - Returns a list of valid ``qp.Batch`` names. - - Parameters - ---------- - batches: str/ list of str, default 'all' - Included names are checked against valid ``qp.Batch`` names. If - batches='all', all valid ``Batch`` names are returned. - - Returns - ------- - list of str - """ - if not batches: - return [] - elif batches[0] == 'all': - return self[dk].meta['sets']['batches'].keys() - else: - valid = self[dk].meta['sets']['batches'].keys() - not_valid = [b for b in batches if not b in valid] - if not_valid: - msg = '``Batch`` name not found in ``Stack``: {}' - raise KeyError(msg.format(not_valid)) - return batches - - def _x_y_f_w_map(self, dk, batches='all'): - """ - """ - def _append_loop(mapping, x, fn, f, w, ys): - if not x in mapping: - mapping[x] = {fn: {'f': f, tuple(w): ys}} - elif not fn in mapping[x]: - mapping[x][fn] = {'f': f, tuple(w): ys} - elif not tuple(w) in mapping[x][fn]: - mapping[x][fn][tuple(w)] = ys - elif not all(y in mapping[x][fn][tuple(w)] for y in ys): - yks = set(mapping[x][fn][tuple(w)]).union(set(ys)) - mapping[x][fn][tuple(w)] = list(yks) - return None - - arrays = self.variable_types(dk, verbose=False)['array'] - mapping = {} - y_on_y = {} - batches = self._check_batches(dk, batches) - for batch in batches: - b = self[dk].meta['sets']['batches'][batch] - xs = b['x_y_map'].keys() - ys = b['x_y_map'] - f = b['x_filter_map'] - w = b['weights'] - fs = b['filter'] - for x in xs: - if x == '@': - for y in ys[x]: - fn = f[y] if f[y] == 'no_filter' else f[y].keys()[0] - _append_loop(mapping, x, fn, f[y], w, ys[x]) - else: - fn = f[x] if f[x] == 'no_filter' else f[x].keys()[0] - _append_loop(mapping, x, fn, f[x], w, ys[x]) - if b['y_on_y']: - fn = fs if fs == 'no_filter' else fs.keys()[0] - for x in b['yks'][1:]: - _append_loop(mapping, x, fn, fs, w, b['yks']) - _append_loop(y_on_y, x, fn, fs, w, b['yks']) - return mapping, y_on_y - - @modify(to_list=['views', 'categorize', 'xs', 'batches']) - def aggregate(self, views, unweighted_base=True, categorize=[], - batches='all', xs=None, verbose=True): - - """ - Add views to all defined ``qp.Link`` in ``qp.Stack``. - - Parameters - ---------- - views: str or list of str or qp.ViewMapper - ``views`` that are added. - unweighted_base: bool, default True - If True, unweighted 'cbase' is added to all non-arrays. - categorize: str or list of str - Determines how numerical data is handled: If provided, the - variables will get counts and percentage aggregations - (``'counts'``, ``'c%'``) alongside the ``'cbase'`` view. If False, - only ``'cbase'`` views are generated for non-categorical types. - batches: str/ list of str, default 'all' - Name(s) of ``qp.Batch`` instance(s) that are used to aggregate the - ``qp.Stack``. - xs: list of str - Names of variable, for which views are added. - - Returns - ------- - None, modify ``qp.Stack`` inplace - """ - if not 'cbase' in views: unweighted_base = False - if isinstance(views[0], ViewMapper): - views = views[0] - complete = views[views.keys()[0]]['kwargs'].get('complete', False) - elif any('cumsum' in v for v in views): - complete = True - else: - complete = False - x_in_stack = self.describe('x').index.tolist() - for dk in self.keys(): - batches = self._check_batches(dk, batches) - if not batches: return None - x_y_f_w_map, y_on_y = self._x_y_f_w_map(dk, batches) - if not xs: - xs = [x for x in x_y_f_w_map.keys() if x in x_in_stack] - else: - xs = [x for x in xs if x in x_in_stack] - v_typ = self.variable_types(dk, verbose=False) - numerics = v_typ['int'] + v_typ['float'] - skipped = [x for x in xs if (x in numerics and not x in categorize)] - total_len = len(xs) - if total_len == 0: - msg = "Cannot aggregate, 'xs' contains no valid variables." - raise ValueError(msg) - for idx, x in enumerate(xs, start=1): - if not x in x_y_f_w_map.keys(): - msg = "Cannot find {} in qp.Stack for ``qp.Batch`` '{}'" - raise KeyError(msg.format(x, batches)) - v = ['cbase'] if x in skipped else views - for f_dict in x_y_f_w_map[x].values(): - f = f_dict.pop('f') - for weight, y in f_dict.items(): - w = list(weight) if weight else None - self.add_link(dk, f, x=x, y=y, views=v, weights=w) - if unweighted_base and not ((None in w and 'cbase' in v) - or x in v_typ['array'] or any(yks in v_typ['array'] for yks in y)): - self.add_link(dk, f, x=x, y=y, views=['cbase'], weights=None) - if complete: - if isinstance(f, dict): - f_key = f.keys()[0] - else: - f_key = f - for ys in y: - y_on_ys = y_on_y.get(x, {}).get(f_key, {}).get(tuple(w), []) - if ys in y_on_ys: continue - link = self[dk][f_key][x][ys] - for ws in w: - pct = 'x|f|:|y|{}|c%'.format('' if not ws else ws) - counts = 'x|f|:||{}|counts'.format('' if not ws else ws) - for view in [pct, counts]: - if view in link: - del link[view] - - if verbose: - done = float(idx) / float(total_len) *100 - print '\r', - time.sleep(0.01) - print 'Stack [{}]: {} %'.format(dk, round(done, 1)), - sys.stdout.flush() - - if skipped and verbose: - msg = ("\n\nWarning: Found {} non-categorized numeric variable(s): {}.\n" - "Descriptive statistics must be added!") - print msg.format(len(skipped), skipped) - return None - - @modify(to_list=['on_vars', '_batches']) - def cumulative_sum(self, on_vars, _batches='all', verbose=True): - """ - Add cumulative sum view to a specified collection of xks of the stack. - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - The stack instance is modified inplace. - """ - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches or not on_vars: return None - meta = self[dk].meta - data = self[dk].data - for v in on_vars: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars + items)) - - self.aggregate(['counts_cumsum', 'c%_cumsum'], False, [], _batches, on_vars, verbose) - return None - - def _add_checking_chain(self, dk, cluster, name, x, y, views): - key, view, c_view = views - c_stack = qp.Stack('checks') - c_stack.add_data('checks', data=self[dk].data, meta=self[dk].meta) - c_stack.add_link(x=x, y=y, views=view, weights=None) - c_stack.add_link(x=x, y=y, views=c_view, weights=None) - c_views = c_stack.describe('view').index.tolist() - len_v_keys = len(view) - view_keys = ['x|f|x:|||cbase', 'x|f|:|||counts'][0:len_v_keys] - c_views = view_keys + [v for v in c_views - if v.endswith('{}_check'.format(key))] - if name == 'stat_check': - chain = c_stack.get_chain(x=x, y=y, views=c_views, orient_on='x') - name = [v for v in c_views if v.endswith('{}_check'.format(key))][0] - cluster[name] = chain - else: - chain = c_stack.get_chain(name=name, x=x, y=y, views=c_views) - cluster.add_chain(chain) - return cluster - - @modify(to_list=['on_vars', '_batches']) - def add_nets(self, on_vars, net_map, expand=None, calc=None, text_prefix='Net:', - checking_cluster=None, _batches='all', verbose=True): - """ - Add a net-like view to a specified collection of x keys of the stack. - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - net_map : list of dicts - The listed dicts must map the net/band text label to lists of - categorical answer codes to group together, e.g.: - - >>> [{'Top3': [1, 2, 3]}, - ... {'Bottom3': [4, 5, 6]}] - It is also possible to provide enumerated net definition dictionaries - that are explicitly setting ``text`` metadata per ``text_key`` entries: - - >>> [{1: [1, 2], 'text': {'en-GB': 'UK NET TEXT', - ... 'da-DK': 'DK NET TEXT', - ... 'de-DE': 'DE NET TEXT'}}] - expand : {'before', 'after'}, default None - If provided, the view will list the net-defining codes after or before - the computed net groups (i.e. "overcode" nets). - calc : dict, default None - A dictionary that is attaching a text label to a calculation expression - using the the net definitions. The nets are referenced as per - 'net_1', 'net_2', 'net_3', ... . - Supported calculation expressions are add, sub, div, mul. Example: - - >>> {'calc': ('net_1', add, 'net_2'), 'text': {'en-GB': 'UK CALC LAB', - ... 'da-DK': 'DA CALC LAB', - ... 'de-DE': 'DE CALC LAB'}} - text_prefix : str, default 'Net:' - By default each code grouping/net will have its ``text`` label prefixed - with 'Net: '. Toggle by passing None (or an empty str, ''). - checking_cluster : quantipy.Cluster, default None - When provided, an automated checking aggregation will be added to the - ``Cluster`` instance. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - The stack instance is modified inplace. - """ - def _netdef_from_map(net_map, expand, prefix, text_key): - netdef = [] - for no, net in enumerate(net_map, start=1): - if 'text' in net: - logic = net[no] - text = net['text'] - else: - logic = net.values()[0] - text = {t: net.keys()[0] for t in text_key} - if not isinstance(logic, list) and isinstance(logic, int): - logic = [logic] - if prefix and not expand: - text = {k: '{} {}'.format(prefix, v) for k, v in text.items()} - if expand: - text = {k: '{} (NET)'.format(v) for k, v in text.items()} - netdef.append({'net_{}'.format(no): logic, 'text': text}) - return netdef - - def _check_and_update_calc(calc_expression, text_key): - if not isinstance(calc_expression, dict): - err_msg = ("'calc' must be a dict in form of\n" - "{'calculation label': (net # 1, operator, net # 2)}") - raise TypeError(err_msg) - for k, v in calc_expression.items(): - if not k in ['text', 'calc_only']: exp = v - if not k == 'calc_only': text = v - if not 'text' in calc_expression: - text = {tk: text for tk in text_key} - calc_expression['text'] = text - if not isinstance(exp, (tuple, list)) or len(exp) != 3: - err_msg = ("Not properly formed expression found in 'calc':\n" - "{}\nMust be provided as (net # 1, operator, net # 2)") - raise TypeError(err_msg.format(exp)) - return calc_expression - - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches: return None - meta = self[dk].meta - data = self[dk].data - for v in on_vars: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars + items)) - all_batches = copy.deepcopy(meta['sets']['batches']) - for n, b in all_batches.items(): - if not n in _batches: all_batches.pop(n) - languages = list(set(b['language'] for n, b in all_batches.items())) - netdef = _netdef_from_map(net_map, expand, text_prefix, languages) - if calc: calc = _check_and_update_calc(calc, languages) - view = qp.ViewMapper() - view.make_template('frequency', {'rel_to': [None, 'y']}) - options = {'logic': netdef, - 'axis': 'x', - 'expand': expand if expand in ['after', 'before'] else None, - 'complete': True if expand else False, - 'calc': calc} - view.add_method('net', kwargs=options) - self.aggregate(view, False, [], _batches, on_vars, verbose) - - if checking_cluster is not None: - c_vars = {v: '{}_net_check'.format(v) for v in on_vars - if not v in meta['sets'] and - not '{}_net_check'.format(v) in checking_cluster.keys()} - view['net_check'] = view.pop('net') - view['net_check']['kwargs']['iterators'].pop('rel_to') - for k, net in c_vars.items(): - checking_cluster = self._add_checking_chain(dk, checking_cluster, - net, k, ['@', k], ('net', ['cbase'], view)) - return None - - - @modify(to_list=['on_vars', 'stats', 'exclude', '_batches']) - def add_stats(self, on_vars, stats=['mean'], other_source=None, rescale=None, - drop=True, exclude=None, factor_labels=True, custom_text=None, - checking_cluster=None, _batches='all', verbose=True): - """ - Add a descriptives view to a specified collection of xks of the stack. - - Valid descriptives views: {'mean', 'stddev', 'min', 'max', 'median', 'sem'} - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - stats : list of str, default ``['mean']`` - The metrics to compute and add as a view. - other_source : str - If provided the Link's x-axis variable will be swapped with the - (numerical) variable provided. This can be used to attach statistics - of a different variable to a Link definition. - rescale : dict - A dict that maps old to new codes, e.g. {1: 5, 2: 4, 3: 3, 4: 2, 5: 1} - drop : bool, default True - If ``rescale`` is provided all codes that are not mapped will be - ignored in the computation. - exclude : list - Codes/values to ignore in the computation. - factor_lables : bool, default True - If True, will write the (rescaled) factor values next to the - category text label. - custom_text : str, default None - A custom string affix to put at the end of the requested statistics' - names. - checking_cluster : quantipy.Cluster, default None - When provided, an automated checking aggregation will be added to the - ``Cluster`` instance. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - The stack instance is modified inplace. - """ - - def _factor_labs(values, rescale, drop, exclude, axis=['x']): - if not rescale: rescale = {} - ignore = [v['value'] for v in values if v['value'] in exclude or - (not v['value'] in rescale.keys() and drop)] - - factors_mapped = {} - for v in values: - if v['value'] in ignore: continue - has_xedits = v['text'].get('x edits', {}) - has_yedits = v['text'].get('y edits', {}) - if not has_xedits: v['text']['x edits'] = {} - if not has_yedits: v['text']['y edits'] = {} - - factor = rescale[v['value']] if rescale else v['value'] - for tk, text in v['text'].items(): - if tk in ['x edits', 'y edits']: continue - for ax in axis: - try: - t = v['text']['{} edits'.format(ax)][tk] - except: - t = text - new_lab = '{} [{}]'.format(t, factor) - v['text']['{} edits'.format(ax)][tk] = new_lab - return values - - if other_source and not isinstance(other_source, str): - raise ValueError("'other_source' must be a str!") - if not rescale: drop = False - - options = {'stats': '', - 'source': other_source, - 'rescale': rescale, - 'drop': drop, 'exclude': exclude, - 'axis': 'x', - 'text': '' if not custom_text else custom_text} - - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches: return None - meta = self[dk].meta - data = self[dk].data - check_on = [] - for v in on_vars: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars + items)) - check_on = list(set(check_on + [items[0]])) - elif not meta['columns'][v].get('values'): - continue - elif not isinstance(meta['columns'][v]['values'], list): - parent = meta['columns'][v]['parent'].keys()[0].split('@')[-1] - items = [i.split('@')[-1] for i in meta['sets'][parent]['items']] - check_on = list(set(check_on + [items[0]])) - else: - check_on = list(set(check_on + [v])) - view = qp.ViewMapper() - view.make_template('descriptives') - for stat in stats: - options['stats'] = stat - view.add_method('stat', kwargs=options) - self.aggregate(view, False, on_vars, _batches, on_vars, verbose) - - if checking_cluster and 'mean' in stats: - options['stats'] = 'mean' - c_view = qp.ViewMapper().make_template('descriptives') - c_view.add_method('stat_check', kwargs=options) - - views = ('stat', ['cbase', 'counts'], c_view) - checking_cluster = self._add_checking_chain(dk, checking_cluster, - 'stat_check', check_on, ['@'], views) - - if not factor_labels or other_source: return None - all_batches = meta['sets']['batches'].keys() - if not _batches: _batches = all_batches - batches = [b for b in all_batches if b in _batches] - - for v in check_on: - globally = False - for b in batches: - batch_me = meta['sets']['batches'][b]['meta_edits'] - values = batch_me.get(v, {}).get('values', []) - if not values: - globally = True - elif not isinstance(values, list): - p = values.split('@')[-1] - values = batch_me['lib'][p] - batch_me['lib'][p] = _factor_labs(values, rescale, drop, - exclude, ['x', 'y']) - else: - batch_me[v]['values'] = _factor_labs(values, rescale, drop, - exclude, ['x']) - if globally: - values = meta['columns'][v]['values'] - if not isinstance(values, list): - p = values.split('@')[-1] - values = meta['lib']['values'][p] - meta['lib']['values'][p] = _factor_labs(values, rescale, drop, - exclude, ['x', 'y']) - else: - meta['columns'][v]['values'] = _factor_labs(values, rescale, - drop, exclude, ['x']) - - return None - - @modify(to_list=['_batches']) - def add_tests(self, _batches='all', verbose=True): - """ - Apply coltests for selected batches. - - Sig. Levels are taken from ``qp.Batch`` definitions. - - Parameters - ---------- - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - """ - self._remove_coltests() - - if verbose: - start = time.time() - - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches: return None - for batch_name in _batches: - batch = self[dk].meta['sets']['batches'][batch_name] - levels = batch['siglevels'] - weight = batch['weights'] - x_y = batch['x_y_map'] - x_f = batch['x_filter_map'] - f = batch['filter'] - yks = batch['yks'] - - if levels: - vm_tests = qp.ViewMapper().make_template( - method='coltests', - iterators={'metric': ['props', 'means'], - 'mimic': ['Dim'], - 'level': levels}) - vm_tests.add_method('significance', - kwargs = {'flag_bases': [30, 100]}) - if 'y_on_y' in batch: - self.add_link(filters=f, x=yks[1:], y=yks, - views=vm_tests, weights=weight) - total_len = len(x_y.keys()) - for idx, x in enumerate(x_y.keys(), 1): - if x == '@': continue - self.add_link(filters=x_f[x], x=x, y=x_y[x], - views=vm_tests, weights=weight) - if verbose: - done = float(idx) / float(total_len) *100 - print '\r', - time.sleep(0.01) - print 'Batch [{}]: {} %'.format(batch_name, round(done, 1)), - sys.stdout.flush() - if verbose: print '\n' - if verbose: print 'Sig-Tests:', time.time()-start - return None - - def _remove_coltests(self, props=True, means=True): - """ - Remove coltests from stack. - - Parameters - ---------- - props : bool, default=True - If True, column proportion test view will be removed from stack. - means : bool, default=True - If True, column mean test view will be removed from stack. - """ - for dk in self.keys(): - for fk in self[dk].keys(): - for xk in self[dk][fk].keys(): - for yk in self[dk][fk][xk].keys(): - for vk in self[dk][fk][xk][yk].keys(): - del_prop = props and 't.props' in vk - del_mean = means and 't.means' in vk - if del_prop or del_mean: - del self[dk][fk][xk][yk][vk] - return None From fe37c5283ce77ae1dddd534ed58f8d905f9b8442 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 3 Aug 2017 09:03:43 +0200 Subject: [PATCH 041/733] change access fill option of bar_chart_series --- core/builds/powerpoint/add_shapes.py | 691 ++++++++++++++------------- 1 file changed, 347 insertions(+), 344 deletions(-) diff --git a/core/builds/powerpoint/add_shapes.py b/core/builds/powerpoint/add_shapes.py index 6b2798468..6c25e44c5 100644 --- a/core/builds/powerpoint/add_shapes.py +++ b/core/builds/powerpoint/add_shapes.py @@ -14,10 +14,10 @@ from pptx.chart.data import ChartData from pptx.dml.color import RGBColor from pptx.enum.chart import( - XL_CHART_TYPE, - XL_LABEL_POSITION, - XL_LEGEND_POSITION, - XL_TICK_MARK, + XL_CHART_TYPE, + XL_LABEL_POSITION, + XL_LEGEND_POSITION, + XL_TICK_MARK, XL_TICK_LABEL_POSITION ) from pptx.util import( @@ -27,13 +27,13 @@ Inches ) from pptx.enum.dml import( - MSO_THEME_COLOR, + MSO_THEME_COLOR, MSO_COLOR_TYPE, MSO_FILL ) from pptx.enum.text import( PP_ALIGN, - MSO_AUTO_SIZE, + MSO_AUTO_SIZE, MSO_ANCHOR ) @@ -79,7 +79,7 @@ 'none': XL_TICK_LABEL_POSITION.NONE} -tick_mark_pos_dct = {'cross': XL_TICK_MARK.CROSS, +tick_mark_pos_dct = {'cross': XL_TICK_MARK.CROSS, 'inside': XL_TICK_MARK.INSIDE, 'none': XL_TICK_MARK.NONE, 'outside': XL_TICK_MARK.OUTSIDE} @@ -125,7 +125,7 @@ def percentage_of_num(percent, whole): """returns percent of a number. e.g. what is 5% of 20 """ - + return (percent * whole) / 100.0 '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' @@ -134,10 +134,10 @@ def percentage_of_num(percent, whole): def get_cht_plot_height(chart_height, percent=10.52395879982087): """calculates a given chart's plot height by the charts height """ - + amounttoremove = percentage_of_num(percent, chart_height) plot_height = chart_height - amounttoremove - + return int(plot_height) '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' @@ -146,44 +146,44 @@ def get_cht_plot_height(chart_height, percent=10.52395879982087): def get_upper_cht_plot_gap(chart_height, percent=3.5078369905956115): """calculates the gap between the top of the plot area and top of chart areas """ - + upper_cht_plot_gap = percentage_of_num(percent, chart_height) - + return int(upper_cht_plot_gap) '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_textbox( - slide, - text, + slide, + text, left=Cm(0.79), top=Cm(2.79), width=Cm(23.84), height=Cm(1.3), font_name="Calibri", - font_size=12, + font_size=12, font_bold=True, font_italic=False, font_color=(89,89,89), font_color_brightness=0, font_color_theme=None, - word_wrap=True, - auto_size=None, + word_wrap=True, + auto_size=None, fit_text=True, font_file=None, margin_left=0.25, margin_right=0.25, margin_top=0.13, - margin_bottom=0.13, + margin_bottom=0.13, vertical_alignment='top', horizontal_alignment='left', textbox_fill_solid=False, textbox_color=(100,0,0), textbox_color_brightness=0, ): - #------------------------------------------------------------------------- - + #------------------------------------------------------------------------- + """Adds textbox and sets it's properties to a given slide. - The only required arguments are the slide and the actual text + The only required arguments are the slide and the actual text content. Optional arguments: @@ -197,57 +197,57 @@ def add_textbox( width |width of the textbox in points (default: 300) ________________________________________________________________________________________________________ height |height of the textbox in points (default: 100) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ font_name |sets font type ________________________________________________________________________________________________________ font_size |font size (default: 12) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ font_bold |turns bold on/off - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ font_italic |turns italics on/off - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ font_color_brightness |range between -1.0 (darker) to 1.0 (lighter) ________________________________________________________________________________________________________ word_wrap |turns word wrapping on/off in the new rectangle (default: True) - ________________________________________________________________________________________________________ - auto_size |determines whether or not to automatically adjust the + ________________________________________________________________________________________________________ + auto_size |determines whether or not to automatically adjust the |height of the textbox to fit all the text |(default: 0 [do no auto-size, NONE, SHAPE_TO_FIT_TEXT, TEXT_TO_FIT_SHAPE]) ________________________________________________________________________________________________________ - alignment |text alignment (default: 1 [left; see PowerPoint's + alignment |text alignment (default: 1 [left; see PowerPoint's |PpParagraphAlignment] ________________________________________________________________________________________________________ - vertical_alignment |vertical text alignment + vertical_alignment |vertical text alignment |(default: 3 [middle; see Office's MsoVerticalAnchor]) ________________________________________________________________________________________________________ """ - + textbox = slide.shapes.add_textbox(left, top, width, height) textframe = textbox.text_frame textframe.vertical_anchor = vertical_alignment_pos_dct[vertical_alignment] - + textframe.margin_left = Cm(margin_left) textframe.margin_bottom = Cm(margin_bottom) textframe.margin_right = Cm(margin_right) textframe.margin_top = Cm(margin_top) - + paragraph = textframe.paragraphs[0] paragraph.font.color.rgb = RGBColor(*font_color) if font_color_theme is not None: paragraph.font.color.theme_color = theme_color_index_dct[font_color_theme] - paragraph.font.color.brightness = font_color_brightness + paragraph.font.color.brightness = font_color_brightness paragraph.alignment = paragraph_alignment_pos_dct[horizontal_alignment] - + textframe.text = text if fit_text == True: if font_name == "Calibri": calibriz = path.join(thisdir, 'fonts\calibriz.ttf') - textframe.fit_text(font_family=font_name, max_size=font_size, bold=font_bold, + textframe.fit_text(font_family=font_name, max_size=font_size, bold=font_bold, italic=font_italic, font_file=calibriz) else: - textframe.fit_text(font_family=font_name, max_size=font_size, bold=font_bold, + textframe.fit_text(font_family=font_name, max_size=font_size, bold=font_bold, italic=font_italic) else: paragraph.font.name = font_name @@ -268,11 +268,11 @@ def add_textbox( '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_pie_chart( - slide, - dataframe, + slide, + dataframe, left=Cm(0.79), top=Cm(4.1), width=Cm(23.84), height=Cm(11.5), - chart_style=2, - + chart_style=2, + #Legend properties has_legend=True, legend_position='bottom', @@ -284,7 +284,7 @@ def add_pie_chart( legend_font_italic=False, legend_font_color=(89,89,89), legend_font_brightness=0, - + #Datalabel properties plot_has_data_labels=True, data_labels_position='outside_end', @@ -295,11 +295,11 @@ def add_pie_chart( data_labels_font_bold=False, data_labels_font_italic=False, data_labels_font_color=(0,0,0), - + #Excel table excel_num_format='0.00%' ): - #------------------------------------------------------------------------- + #------------------------------------------------------------------------- """Adds single series pie chart to a given slide and set it's properties. The only required arguments are 'slide' and 'dataframe' content. @@ -309,7 +309,7 @@ def add_pie_chart( left |The distance, in EMU points, from left edge of Slide area. |to the left edge of the chart area. (default: 144000). ________________________________________________________________________________________________________ - top |The distance, in EMU points, from the top edge of the + top |The distance, in EMU points, from the top edge of the |Slide area to the top of the chart area. (default: 1584000). ________________________________________________________________________________________________________ width |Width of the chart in EMU points (default: 8838000). @@ -318,25 +318,25 @@ def add_pie_chart( height |Height of the chart in EMU points (default: 4320000). | ________________________________________________________________________________________________________ - chart_style |Sets the chart style for the chart. + chart_style |Sets the chart style for the chart. |You can use a number from 1 to 48 to set the chart style (default: 2). ________________________________________________________________________________________________________ - has_legend |Boolean. True if the chart has a legend. + has_legend |Boolean. True if the chart has a legend. | ________________________________________________________________________________________________________ legend_position |Sets the position of the legend on the chart. |[bottom, corner, custom, left, right, top] (default: bottom) ________________________________________________________________________________________________________ - legend_in_layout |Boolean. True if a legend will occupy the chart layout space when + legend_in_layout |Boolean. True if a legend will occupy the chart layout space when |a chart layout is being determined. ________________________________________________________________________________________________________ - legend_horz_offset |Adjustment of the x position of the legend from its default. Expressed + legend_horz_offset |Adjustment of the x position of the legend from its default. Expressed |as a float between -1.0 and 1.0 representing a fraction of the chart width. ________________________________________________________________________________________________________ - legend_font_name |Set the typeface name for this Font instance, causing the text it + legend_font_name |Set the typeface name for this Font instance, causing the text it |controls to appear in the named font, if a matching font is found. ________________________________________________________________________________________________________ - legend_font_size |Set length value or None, indicating the font height + legend_font_size |Set length value or None, indicating the font height |in EMU. ________________________________________________________________________________________________________ legend_font_bold |Boolean. Set boolean bold value of Font @@ -348,14 +348,14 @@ def add_pie_chart( legend_font_color |Set color for this lengend's font in RGBColor(r, g, b) | ________________________________________________________________________________________________________ - legend_font_brightness |Set float value between -1.0 and 1.0 indicating the brightness adjustment + legend_font_brightness |Set float value between -1.0 and 1.0 indicating the brightness adjustment |for this color, e.g. -0.25 is 25 percent darker and 0.4 is 40 percent lighter. |0 means no brightness adjustment. ________________________________________________________________________________________________________ - plot_has_data_labels |Boolean. Set if the series has data labels. + plot_has_data_labels |Boolean. Set if the series has data labels. |Assigning True causes data labels to be added to the plot ________________________________________________________________________________________________________ - data_labels_position |value specifying the position of the data labels + data_labels_position |value specifying the position of the data labels |with respect to their data point, or None if no position is specified |[outside_end, above, below, best_fit, center, inside_base, inside_end, |left, mixed, outside_end, right] (default: outside_end). @@ -363,49 +363,49 @@ def add_pie_chart( data_labels_num_format_is_linked |Set string specifying the format for the numbers on this set of data labels |Returns 'General' if no number format has been set. Note that this format string |has no effect on rendered data labels when number_format_is_linked() is True - ________________________________________________________________________________________________________ - data_labels_font_name |Set the typeface name for this Font instance, causing the text it + ________________________________________________________________________________________________________ + data_labels_font_name |Set the typeface name for this Font instance, causing the text it |controls to appear in the named font, if a matching font is found. - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ data_labels_font_size |Set length value or None, indicating the font height |in EMU. - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ data_labels_font_bold |Boolean. Set boolean bold value of Font | - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ data_labels_font_italic |Boolean. Set boolean italic value of Font | - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ data_labels_font_color |Set color for this data lebel's font in RGBColor(r, g, b) | - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ """ - #strips html code + #strips html code dataframe = clean_axes_labels(dataframe) - + # Adding chart data chart_data = ChartData() chart_data.categories = dataframe.index for i, col in enumerate(dataframe.columns): chart_data.add_series(col, (dataframe.ix[:, i].values), excel_num_format) - + # Adding chart - x, y, cx, cy = left, top, width, height + x, y, cx, cy = left, top, width, height graphic_frame = slide.shapes.add_chart( XL_CHART_TYPE.PIE, x, y, cx, cy, chart_data ) chart = graphic_frame.chart - - #---------------- adjust chart properties ------------------------------- - + + #---------------- adjust chart properties ------------------------------- + chart.chart_style = chart_style - + # set legend properties chart.has_legend = has_legend if has_legend: - legend = chart.legend + legend = chart.legend legend.font.name = legend_font_name legend.font.size = Pt(legend_font_size) legend.font.bold = legend_font_bold @@ -416,12 +416,12 @@ def add_pie_chart( legend.include_in_layout = legend_in_layout legend.horz_offset = legend_horz_offset - plot = chart.plots[0] + plot = chart.plots[0] plot.has_data_labels = plot_has_data_labels - + # set datalabel properties if plot_has_data_labels: - data_labels = plot.data_labels + data_labels = plot.data_labels data_labels.position = data_label_pos_dct[data_labels_position] data_labels.font.name = data_labels_font_name data_labels.font.size = Pt(data_labels_font_size) @@ -436,13 +436,13 @@ def add_pie_chart( '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_bar_chart( - slide, - dataframe, + slide, + dataframe, left=Cm(0.79), top=Cm(4.1), width=Cm(23.84), height=Cm(11.5), - chart_style=2, - + chart_style=2, + #Legend properties - has_legend=True, + has_legend=True, legend_position='right', legend_in_layout=False, legend_horz_offset = 0.1583, @@ -452,40 +452,40 @@ def add_bar_chart( legend_font_italic=False, legend_font_color=(89,89,89), legend_font_brightness=0, - + #Category axis properties caxis_visible=True, caxis_tick_label_position='low', caxis_tick_labels_offset=730, caxis_has_major_gridlines=False, caxis_has_minor_gridlines=False, - caxis_major_tick_mark='outside', + caxis_major_tick_mark='outside', caxis_minor_tick_mark='none', caxis_tick_labels_font_name="Calibri", caxis_tick_labels_font_size=10, caxis_tick_labels_font_bold=False, caxis_tick_labels_font_italic=False, caxis_tick_labels_font_color=(89,89,89), - + #Value axis properties - vaxis_visible=True, + vaxis_visible=True, vaxis_tick_label_position='low', vaxis_has_major_gridlines=True, vaxis_has_minor_gridlines=False, vaxis_major_tick_mark='outside', - vaxis_minor_tick_mark='none', - vaxis_max_scale=1, + vaxis_minor_tick_mark='none', + vaxis_max_scale=1, vaxis_min_scale=0, vaxis_major_unit=0.1, vaxis_minor_unit=None, - vaxis_tick_labels_num_format='0%', + vaxis_tick_labels_num_format='0%', vaxis_tick_labels_num_format_is_linked=False, vaxis_tick_labels_font_name="Calibri", vaxis_tick_labels_font_bold=True, vaxis_tick_labels_font_size=10, vaxis_tick_labels_font_italic=False, vaxis_tick_labels_font_color=(89,89,89), - + #Datalabel properties plot_has_data_labels=True, data_labels_position='outside_end', @@ -496,7 +496,7 @@ def add_bar_chart( data_labels_font_bold=False, data_labels_font_italic=False, data_labels_font_color=(0,0,0), - + #Plot properties plot_vary_by_cat=False, series_color_order='reverse', @@ -505,11 +505,11 @@ def add_bar_chart( plot_overlap=-10, series_line_color=None, series_line_width=None, - + #Excel table excel_num_format='0.00%' ): - #------------------------------------------------------------------------- + #------------------------------------------------------------------------- """Adds single or multi series bar chart to a given slide and set it's properties. The only required arguments are slide and dataframe content. @@ -526,27 +526,27 @@ def add_bar_chart( ________________________________________________________________________________________________________ height |height of the chart in points (default: 4466000) ________________________________________________________________________________________________________ - chart_style |available options on the Chart Styles group on + chart_style |available options on the Chart Styles group on |the Design tab on the Ribbon (default: 2) ________________________________________________________________________________________________________ has_legend |turns on/off the legend for chart (default: True) ________________________________________________________________________________________________________ - legend_in_layout |determines if a legend will occupy the chart layout + legend_in_layout |determines if a legend will occupy the chart layout |space when a chart layout is being determined (default: False) - ________________________________________________________________________________________________________ - legend_position |determines the position of a legend + ________________________________________________________________________________________________________ + legend_position |determines the position of a legend |(default: XL_LEGEND_POSITION.RIGHT [TOP, BOTTOM,LEFT,RIGHT,TOP_RIGHT]) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ legend_horz_offset |sets the horizontal distance between the plot area and end of chart area |(default: 0.1583) ________________________________________________________________________________________________________ caxis_visible |determines if the category labels are visible or not (default: False) - ________________________________________________________________________________________________________ - caxis_has_major_gridlines |turns major gridlines on/off for category axis - ________________________________________________________________________________________________________ - caxis_has_minor_gridlines |turns minor gridlines on/off for category axis ________________________________________________________________________________________________________ - caxis_major_tick_mark |draws marks at the same point along an axis as a data point + caxis_has_major_gridlines |turns major gridlines on/off for category axis + ________________________________________________________________________________________________________ + caxis_has_minor_gridlines |turns minor gridlines on/off for category axis + ________________________________________________________________________________________________________ + caxis_major_tick_mark |draws marks at the same point along an axis as a data point |(default: XL_TICK_MARK.NONE [OUTSIDE, INSIDE, CROSS, NONE]) ________________________________________________________________________________________________________ caxis_minor_tick_mark |draws marks in between major tick marks, where labels usually occur @@ -555,26 +555,26 @@ def add_bar_chart( caxis_tick_labels_offset |label spacing of the category axis (default: 730) ________________________________________________________________________________________________________ vaxis_visible |determines if the category labels are visible or not (default: True) - ________________________________________________________________________________________________________ - vaxis_has_major_gridlines |turns major gridlines on/off for value axis - ________________________________________________________________________________________________________ - vaxis_has_minor_gridlines |turns minor gridlines on/off for value axis + ________________________________________________________________________________________________________ + vaxis_has_major_gridlines |turns major gridlines on/off for value axis + ________________________________________________________________________________________________________ + vaxis_has_minor_gridlines |turns minor gridlines on/off for value axis ________________________________________________________________________________________________________ vaxis_max_scale |sets maximum values for the value axis (default: 100.0) ________________________________________________________________________________________________________ vaxis_min_scale |sets minimum values for the value axis (default: 0.0) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ vaxis_major_unit |sets maximum values for the value axis (default: 10.0) ________________________________________________________________________________________________________ - vaxis_minor_unit |sets minimum values for the value axis (default: None) + vaxis_minor_unit |sets minimum values for the value axis (default: None) ________________________________________________________________________________________________________ - vaxis_minor_tick_mark |draws marks at the same point along an axis as a data point + vaxis_minor_tick_mark |draws marks at the same point along an axis as a data point |(default: XL_TICK_MARK.NONE [OUTSIDE, INSIDE, CROSS, NONE]) ________________________________________________________________________________________________________ - vaxis_major_tick_mark |draws the minor tick marks for the value axis in Chart to be inside + vaxis_major_tick_mark |draws the minor tick marks for the value axis in Chart to be inside |the axis (default: XL_TICK_MARK.OUTSIDE, [OUTSIDE, INSIDE, CROSS, NONE]) ________________________________________________________________________________________________________ - vaxis_num_format |sets the number format for the tick-mark labels on the value axis in + vaxis_num_format |sets the number format for the tick-mark labels on the value axis in |the chart. (default: '0"%"') ________________________________________________________________________________________________________ vaxis_font_bold |sets font to bold on the value axis in chart (default: True) @@ -583,66 +583,66 @@ def add_bar_chart( _______________________________________________________________________________________________________ plot_has_data_labels |enables data labels for series in chart (default: True) ________________________________________________________________________________________________________ - data_labels_position |determines position of data labels + data_labels_position |determines position of data labels |(default:XL_LABEL_POSITION.OUTSIDE_END [CENTER, INSIDE_END, INSIDE_BASE]) ________________________________________________________________________________________________________ - data_label_num_format |sets the number format for the data labels on series plot in + data_label_num_format |sets the number format for the data labels on series plot in |the chart. (default: None ['0"%"']) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ invert_if_negative |inverts bar colour for negative series (default: False [same colour as rest]) - ________________________________________________________________________________________________________ + ________________________________________________________________________________________________________ plot_gap_width |determine space between bars (default: 100 [range between 0 to 500) ________________________________________________________________________________________________________ - plot_overlap |You can set this property to a value from -100 through 100. If this - |property is set to -100, bars are positioned so that there is one bar width - |between them. If the overlap is 0 (zero), there is no space between bars - |(one bar starts immediately after the preceding bar). If the overlap + plot_overlap |You can set this property to a value from -100 through 100. If this + |property is set to -100, bars are positioned so that there is one bar width + |between them. If the overlap is 0 (zero), there is no space between bars + |(one bar starts immediately after the preceding bar). If the overlap |is 100, bars are positioned on top of each other. |(default: -10) ________________________________________________________________________________________________________ """ - - #strips html code + + #strips html code dataframe = clean_axes_labels(dataframe) #if category labels are split from the chart shape then determine the width of the textboxes and the chart shape. - #textboxes in this case will take up 40% of the overall width of the chart shape. From this we can calculate the - #width of the chart shape - + #textboxes in this case will take up 40% of the overall width of the chart shape. From this we can calculate the + #width of the chart shape + if (caxis_visible == False) or (caxis_visible == True and str(tick_label_pos_dct[caxis_tick_label_position]) == "NONE (-4142)"): - catwidth = percentage_of_num(40, width) + catwidth = percentage_of_num(40, width) width = width - catwidth left = left + catwidth - # orientation of chart type requires that we reverse the row and column order. + # orientation of chart type requires that we reverse the row and column order. dataframe = dataframe[::-1] dataframe = dataframe[dataframe.columns[::-1]] # add chart data chart_data = ChartData() chart_data.categories = dataframe.index - + for i, col in enumerate(dataframe.columns): chart_data.add_series(col, (dataframe.ix[:, i].values), excel_num_format) - + # add chart - x, y, cx, cy = left, top, width, height + x, y, cx, cy = left, top, width, height graphic_frame = slide.shapes.add_chart( XL_CHART_TYPE.BAR_CLUSTERED, x, y, cx, cy, chart_data ) chart = graphic_frame.chart - + # ---------------- adjust chart properties ---------------- - - # chart style + + # chart style chart.chart_style = chart_style - + # set legend properties - chart.has_legend = has_legend + chart.has_legend = has_legend if has_legend: - legend = chart.legend - legend.include_in_layout = legend_in_layout - legend.position = legend_pos_dct[legend_position] + legend = chart.legend + legend.include_in_layout = legend_in_layout + legend.position = legend_pos_dct[legend_position] legend.horz_offset = legend_horz_offset legend.font.name = legend_font_name legend.font.size = Pt(legend_font_size) @@ -651,14 +651,14 @@ def add_bar_chart( legend.font.color.rgb = RGBColor(*legend_font_color) legend.font.color.brightness = legend_font_brightness - # set category axis (vertical) properties + # set category axis (vertical) properties category_axis = chart.category_axis category_axis.has_major_gridlines = caxis_has_major_gridlines - category_axis.has_minor_gridlines = caxis_has_minor_gridlines + category_axis.has_minor_gridlines = caxis_has_minor_gridlines category_axis.major_tick_mark = tick_mark_pos_dct[caxis_major_tick_mark] category_axis.minor_tick_mark = tick_mark_pos_dct[caxis_minor_tick_mark] - category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] - + category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] + category_axis.visible = caxis_visible if caxis_visible: caxis_tick_labels = category_axis.tick_labels @@ -669,8 +669,8 @@ def add_bar_chart( caxis_tick_labels.font.italic = caxis_tick_labels_font_italic caxis_tick_labels.font.color.rgb = RGBColor(*caxis_tick_labels_font_color) - # set value axis (horizontal) properties - value_axis = chart.value_axis + # set value axis (horizontal) properties + value_axis = chart.value_axis value_axis.has_major_gridlines = vaxis_has_major_gridlines value_axis.has_minor_gridlines = vaxis_has_minor_gridlines value_axis.maximum_scale = vaxis_max_scale @@ -679,7 +679,7 @@ def add_bar_chart( value_axis.minor_unit = vaxis_minor_unit value_axis.major_tick_mark = tick_mark_pos_dct[vaxis_major_tick_mark] value_axis.minor_tick_mark = tick_mark_pos_dct[vaxis_minor_tick_mark] - value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] + value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] value_axis.visible = vaxis_visible if vaxis_visible: @@ -693,15 +693,15 @@ def add_bar_chart( vaxis_tick_labels.number_format = vaxis_tick_labels_num_format vaxis_tick_labels.number_format_is_linked = vaxis_tick_labels_num_format_is_linked - # set plot area properties - plot = chart.plots[0] + # set plot area properties + plot = chart.plots[0] plot.vary_by_categories = plot_vary_by_cat plot.gap_width = plot_gap_width - plot.overlap = plot_overlap - + plot.overlap = plot_overlap + plot.has_data_labels = plot_has_data_labels if plot_has_data_labels: - data_labels = plot.data_labels + data_labels = plot.data_labels data_labels.position = data_label_pos_dct[data_labels_position] data_labels.font.size = Pt(data_labels_font_size) data_labels.font.bold = data_labels_font_bold @@ -711,14 +711,14 @@ def add_bar_chart( if data_labels_num_format is not None: data_labels.number_format = data_labels_num_format data_labels.number_format_is_linked = data_labels_num_format_is_linked - + if series_color_order and len(dataframe.columns) > 1: ser_colors_list = color_setter(len(dataframe.columns), series_color_order) - - for i, ser in enumerate(dataframe.columns): - ser = plot.series[i] + + for i, ser in enumerate(dataframe.columns): + ser = plot.series[i] ser.invert_if_negative = invert_series_color_if_negative - + if series_line_color is not None and series_line_width is not None: ser.line.color.rgb = RGBColor(*series_line_color) ser.line.width = Pt(series_line_width) @@ -727,11 +727,14 @@ def add_bar_chart( ser.line.color.rgb = RGBColor(*series_line_color) if series_color_order and len(dataframe.columns) > 1: - fill = ser.fill - fill.solid() - color_code = ser_colors_list[i] - fill.fore_color.rgb = RGBColor(*color_code) - + try: + fill = ser.fill + except: + fill = ser.format.fill + fill.solid() + color_code = ser_colors_list[i] + fill.fore_color.rgb = RGBColor(*color_code) + # generate overlay axis labels if (caxis_visible == False) or (caxis_visible == True and str(tick_label_pos_dct[caxis_tick_label_position]) == "NONE (-4142)"): cht_plot_height = get_cht_plot_height(height) @@ -739,36 +742,36 @@ def add_bar_chart( rightofchart = left + width txtbx_width = width / 5 firstposition = top + get_upper_cht_plot_gap(height) - + cat_labels = dataframe.T.columns - + for i, label in enumerate(cat_labels): - - top = 0 + + top = 0 pointRelPos = len(cat_labels) - (i + 1) top = firstposition + pointRelPos * heightPerLabel - - add_textbox(slide, + + add_textbox(slide, left=142875, top=top, width=rightofchart - width, height=heightPerLabel, - text=label, + text=label, font_name=caxis_tick_labels_font_name, - font_size=caxis_tick_labels_font_size, + font_size=caxis_tick_labels_font_size, fit_text=False, - word_wrap=True, - font_bold=False, + word_wrap=True, + font_bold=False, font_color=caxis_tick_labels_font_color, - horizontal_alignment='right', + horizontal_alignment='right', vertical_alignment='middle') '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_column_chart( - slide, - dataframe, + slide, + dataframe, left=Cm(0.79), top=Cm(4.1), width=Cm(23.84), height=Cm(11.5), - chart_style=2, - + chart_style=2, + #Legend properties has_legend=True, legend_position='bottom', @@ -780,40 +783,40 @@ def add_column_chart( legend_font_italic=False, legend_font_color=(89,89,89), legend_font_brightness=0, - + #Category axis properties caxis_visible=True, caxis_tick_label_position='next_to_axis', caxis_tick_labels_offset=100, caxis_has_major_gridlines=False, caxis_has_minor_gridlines=False, - caxis_major_tick_mark='outside', + caxis_major_tick_mark='outside', caxis_minor_tick_mark='none', caxis_tick_labels_font_name="Calibri", caxis_tick_labels_font_size=10, caxis_tick_labels_font_bold=False, caxis_tick_labels_font_italic=False, caxis_tick_labels_font_color=(89,89,89), - + #Value axis properties vaxis_visible=True, vaxis_tick_label_position='low', vaxis_has_major_gridlines=True, vaxis_has_minor_gridlines=False, vaxis_major_tick_mark='outside', - vaxis_minor_tick_mark='none', - vaxis_max_scale=1.0, - vaxis_min_scale=0.0, + vaxis_minor_tick_mark='none', + vaxis_max_scale=1.0, + vaxis_min_scale=0.0, vaxis_major_unit=0.1, vaxis_minor_unit=None, - vaxis_tick_labels_num_format='0%', + vaxis_tick_labels_num_format='0%', vaxis_tick_labels_num_format_is_linked=False, vaxis_tick_labels_font_name="Calibri", vaxis_tick_labels_font_size=10, vaxis_tick_labels_font_bold=True, vaxis_tick_labels_font_italic=False, vaxis_tick_labels_font_color=(89,89,89), - + #Datalabel properties plot_has_data_labels=True, data_labels_position='outside_end', @@ -824,48 +827,48 @@ def add_column_chart( data_labels_font_bold=False, data_labels_font_italic=False, data_labels_font_color=(0,0,0), - + #Plot properties - plot_vary_by_cat=False, + plot_vary_by_cat=False, invert_series_color_if_negative=False, plot_gap_width=150, plot_overlap=-10, series_line_color=None, series_line_width=None, - + #Excel table excel_num_format='0.00%' ): - #------------------------------------------------------------------------- - - #strips html code + #------------------------------------------------------------------------- + + #strips html code dataframe = clean_axes_labels(dataframe) # add chart data chart_data = ChartData() chart_data.categories = dataframe.index - + for col in dataframe.columns: chart_data.add_series(col, (dataframe[col].values), excel_num_format) - + # add chart - x, y, cx, cy = left, top, width, height + x, y, cx, cy = left, top, width, height graphic_frame = slide.shapes.add_chart( XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data ) chart = graphic_frame.chart # ---------------- adjust chart properties ---------------- - - # chart style + + # chart style chart.chart_style = chart_style - + # set legend properties chart.has_legend = has_legend if has_legend: - legend = chart.legend - legend.include_in_layout = legend_in_layout - legend.position = legend_pos_dct[legend_position] + legend = chart.legend + legend.include_in_layout = legend_in_layout + legend.position = legend_pos_dct[legend_position] legend.horz_offset = legend_horz_offset legend.font.name = legend_font_name legend.font.size = Pt(legend_font_size) @@ -874,14 +877,14 @@ def add_column_chart( legend.font.color.rgb = RGBColor(*legend_font_color) legend.font.color.brightness = legend_font_brightness - # set category axis (horizontal) properties + # set category axis (horizontal) properties category_axis = chart.category_axis category_axis.has_major_gridlines = caxis_has_major_gridlines category_axis.has_minor_gridlines = caxis_has_minor_gridlines category_axis.major_tick_mark = tick_mark_pos_dct[caxis_major_tick_mark] category_axis.minor_tick_mark = tick_mark_pos_dct[caxis_minor_tick_mark] category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] - + category_axis.visible = caxis_visible if caxis_visible: caxis_tick_labels = category_axis.tick_labels @@ -892,7 +895,7 @@ def add_column_chart( caxis_tick_labels.font.italic = caxis_tick_labels_font_italic caxis_tick_labels.font.color.rgb = RGBColor(*caxis_tick_labels_font_color) - # set value axis (vertical) properties + # set value axis (vertical) properties value_axis = chart.value_axis value_axis.has_major_gridlines = vaxis_has_major_gridlines value_axis.has_minor_gridlines = vaxis_has_minor_gridlines @@ -902,9 +905,9 @@ def add_column_chart( value_axis.minimum_scale = vaxis_min_scale value_axis.major_unit = vaxis_major_unit value_axis.minor_unit = vaxis_minor_unit - value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] - - value_axis.visible = vaxis_visible + value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] + + value_axis.visible = vaxis_visible if vaxis_visible: vaxis_tick_labels = value_axis.tick_labels vaxis_tick_labels.font.bold = vaxis_tick_labels_font_bold @@ -916,15 +919,15 @@ def add_column_chart( vaxis_tick_labels.number_format = vaxis_tick_labels_num_format vaxis_tick_labels.number_format_is_linked = vaxis_tick_labels_num_format_is_linked - # set plot area properties - plot = chart.plots[0] + # set plot area properties + plot = chart.plots[0] plot.vary_by_categories = plot_vary_by_cat plot.gap_width = plot_gap_width plot.overlap = plot_overlap - + plot.has_data_labels = plot_has_data_labels if plot_has_data_labels: - data_labels = plot.data_labels + data_labels = plot.data_labels data_labels.position = data_label_pos_dct[data_labels_position] data_labels.font.size = Pt(data_labels_font_size) data_labels.font.bold = data_labels_font_bold @@ -934,10 +937,10 @@ def add_column_chart( if data_labels_num_format is not None: data_labels.number_format = data_labels_num_format data_labels.number_format_is_linked = data_labels_num_format_is_linked - - for i, ser in enumerate(dataframe.columns): - ser = plot.series[i] - ser.invert_if_negative = invert_series_color_if_negative + + for i, ser in enumerate(dataframe.columns): + ser = plot.series[i] + ser.invert_if_negative = invert_series_color_if_negative if series_line_color is not None and series_line_width is not None: ser.line.color.rgb = RGBColor(*series_line_color) @@ -945,16 +948,16 @@ def add_column_chart( elif series_line_color is not None and series_line_width is None: ser.line.color.rgb = RGBColor(*series_line_color) - + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_line_chart( - slide, - dataframe, + slide, + dataframe, left=Cm(0.79), top=Cm(4.1), width=Cm(23.84), height=Cm(11.5), chart_style=2, - + #Legend properties has_legend=True, legend_position='bottom', @@ -966,7 +969,7 @@ def add_line_chart( legend_font_italic=False, legend_font_color=(89,89,89), legend_font_brightness=0, - + #Category axis properties caxis_visible=True, caxis_tick_label_position='low', @@ -980,14 +983,14 @@ def add_line_chart( caxis_tick_labels_font_italic=False, caxis_tick_labels_font_bold=False, caxis_tick_labels_font_color=(89,89,89), - + #Value axis properties vaxis_visible=True, vaxis_tick_label_position='low', vaxis_has_major_gridlines=True, vaxis_has_minor_gridlines=False, vaxis_major_tick_mark='outside', - vaxis_minor_tick_mark='none', + vaxis_minor_tick_mark='none', vaxis_max_scale=1.0, vaxis_min_scale=0.0, vaxis_major_unit=0.1, @@ -999,7 +1002,7 @@ def add_line_chart( vaxis_tick_labels_font_size=10, vaxis_tick_labels_font_italic=False, vaxis_tick_labels_font_color=(89,89,89), - + #Data label properties plot_has_data_labels=True, data_labels_position='above', @@ -1010,47 +1013,47 @@ def add_line_chart( data_labels_font_bold=False, data_labels_font_italic=False, data_labels_font_color=(0,0,0), - + #Plot properties plot_vary_by_cat=False, invert_series_color_if_negative=False, plot_gap_width=150, plot_overlap=-10, smooth_line=False, - + #Excel table excel_num_format='0.00%' ): - #------------------------------------------------------------------------- - - #strips html code + #------------------------------------------------------------------------- + + #strips html code dataframe = clean_axes_labels(dataframe) # Adding chart data chart_data = ChartData() chart_data.categories = dataframe.index - + for i, col in enumerate(dataframe.columns): chart_data.add_series(col, (dataframe.ix[:, i].values), excel_num_format) - + # Adding chart - x, y, cx, cy = left, top, width, height + x, y, cx, cy = left, top, width, height graphic_frame = slide.shapes.add_chart( XL_CHART_TYPE.LINE, x, y, cx, cy, chart_data ) chart = graphic_frame.chart # ---------------- adjust chart properties ---------------- - - #chart style + + #chart style chart.chart_style = chart_style - + #set legend properties chart.has_legend = has_legend if has_legend: - legend = chart.legend - legend.include_in_layout = legend_in_layout - legend.position = legend_pos_dct[legend_position] + legend = chart.legend + legend.include_in_layout = legend_in_layout + legend.position = legend_pos_dct[legend_position] legend.horz_offset = legend_horz_offset legend.font.name = legend_font_name legend.font.size = Pt(legend_font_size) @@ -1059,14 +1062,14 @@ def add_line_chart( legend.font.color.rgb = RGBColor(*legend_font_color) legend.font.color.brightness = legend_font_brightness - #set category axis (horizontal) properties + #set category axis (horizontal) properties category_axis = chart.category_axis category_axis.has_major_gridlines = caxis_has_major_gridlines category_axis.has_minor_gridlines = caxis_has_minor_gridlines category_axis.major_tick_mark = tick_mark_pos_dct[caxis_major_tick_mark] category_axis.minor_tick_mark = tick_mark_pos_dct[caxis_minor_tick_mark] category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] - + category_axis.visible = caxis_visible if caxis_visible: caxis_tick_labels = category_axis.tick_labels @@ -1077,7 +1080,7 @@ def add_line_chart( caxis_tick_labels.font.italic = caxis_tick_labels_font_italic caxis_tick_labels.font.color.rgb = RGBColor(*caxis_tick_labels_font_color) - # set value axis (vertical) properties + # set value axis (vertical) properties value_axis = chart.value_axis value_axis.has_major_gridlines = vaxis_has_major_gridlines value_axis.has_minor_gridlines = vaxis_has_minor_gridlines @@ -1087,9 +1090,9 @@ def add_line_chart( value_axis.minimum_scale = vaxis_min_scale value_axis.major_unit = vaxis_major_unit value_axis.minor_unit = vaxis_minor_unit - value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] - - value_axis.visible = vaxis_visible + value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] + + value_axis.visible = vaxis_visible if vaxis_visible: vaxis_tick_labels = value_axis.tick_labels vaxis_tick_labels.font.name = vaxis_tick_labels_font_name @@ -1100,16 +1103,16 @@ def add_line_chart( if vaxis_tick_labels_num_format is not None: vaxis_tick_labels.number_format = vaxis_tick_labels_num_format vaxis_tick_labels.number_format_is_linked = vaxis_tick_labels_num_format_is_linked - - # set plot area properties - plot = chart.plots[0] + + # set plot area properties + plot = chart.plots[0] plot.vary_by_categories = plot_vary_by_cat plot.gap_width = plot_gap_width plot.overlap = plot_overlap - + plot.has_data_labels = plot_has_data_labels if plot_has_data_labels: - data_labels = plot.data_labels + data_labels = plot.data_labels data_labels.position = data_label_pos_dct[data_labels_position] data_labels.font.size = Pt(data_labels_font_size) data_labels.font.bold = data_labels_font_bold @@ -1119,21 +1122,21 @@ def add_line_chart( if data_labels_num_format is not None: data_labels.number_format = data_labels_num_format data_labels.number_format_is_linked = data_labels_num_format_is_linked - - for i, ser in enumerate(dataframe.columns): - ser = plot.series[i] + + for i, ser in enumerate(dataframe.columns): + ser = plot.series[i] ser.invert_if_negative = invert_series_color_if_negative - ser.smooth = smooth_line + ser.smooth = smooth_line '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_stacked_bar_chart( - slide, - dataframe, + slide, + dataframe, left=Cm(0.79), top=Cm(4.1), width=Cm(23.84), height=Cm(11.5), chart_style=2, - + #Legend properties has_legend=True, legend_position='right', @@ -1159,7 +1162,7 @@ def add_stacked_bar_chart( caxis_tick_labels_font_bold=False, caxis_tick_labels_font_italic=False, caxis_tick_labels_font_color=(89,89,89), - + #Value axis properties vaxis_visible=True, vaxis_tick_label_position='low', @@ -1171,14 +1174,14 @@ def add_stacked_bar_chart( vaxis_min_scale=0, vaxis_major_unit=0.1, vaxis_minor_unit=None, - vaxis_tick_labels_num_format='0%', + vaxis_tick_labels_num_format='0%', vaxis_tick_labels_num_format_is_linked=False, vaxis_tick_labels_font_name="Calibri", vaxis_tick_labels_font_size=10, vaxis_tick_labels_font_bold=True, vaxis_tick_labels_font_italic=False, vaxis_tick_labels_font_color=(89,89,89), - + #Datalabel properties plot_has_data_labels=True, data_labels_position='center', @@ -1189,60 +1192,60 @@ def add_stacked_bar_chart( data_labels_font_bold=False, data_labels_font_italic=False, data_labels_font_color=(0,0,0), - + #Plot properties invert_series_color_if_negative=False, plot_gap_width=150, plot_overlap=100, series_line_color=None, series_line_width=None, - + #Excel table excel_num_format='0.00%' ): - #------------------------------------------------------------------------- - - #strips html code + #------------------------------------------------------------------------- + + #strips html code dataframe = clean_axes_labels(dataframe) #if category labels are split from chart shape then determine the width of the textboxes and adjust chart shape accordingly. - #textboxes in this case will take up 30% of the overall width of the chart shape. From this we can calculate the - #width of the chart shape. - + #textboxes in this case will take up 30% of the overall width of the chart shape. From this we can calculate the + #width of the chart shape. + if (caxis_visible == False) or (caxis_visible == True and str(tick_label_pos_dct[caxis_tick_label_position]) == "NONE (-4142)"): - catwidth = percentage_of_num(30, width) + catwidth = percentage_of_num(30, width) width = width - catwidth left = left + catwidth - # orientation of chart type requires that we flip/transpose the table and reverse the rows + # orientation of chart type requires that we flip/transpose the table and reverse the rows dataframe = dataframe.T dataframe = dataframe[::-1] # add data chart_data = ChartData() chart_data.categories = dataframe.index - + for i, col in enumerate(dataframe.columns): chart_data.add_series(col, (dataframe.ix[:, i].values), excel_num_format) - + # add chart to slide - x, y, cx, cy = left, top, width, height + x, y, cx, cy = left, top, width, height graphic_frame = slide.shapes.add_chart( XL_CHART_TYPE.BAR_STACKED_100, x, y, cx, cy, chart_data ) chart = graphic_frame.chart # ---------------- adjust chart properties ---------------- - - # chart style + + # chart style chart.chart_style = chart_style - + # set legend properties chart.has_legend = has_legend if has_legend: - legend = chart.legend - legend.include_in_layout = legend_in_layout - legend.position = legend_pos_dct[legend_position] + legend = chart.legend + legend.include_in_layout = legend_in_layout + legend.position = legend_pos_dct[legend_position] legend.horz_offset = legend_horz_offset legend.font.name = legend_font_name legend.font.size = Pt(legend_font_size) @@ -1250,18 +1253,18 @@ def add_stacked_bar_chart( legend.font.italic = legend_font_italic legend.font.color.rgb = RGBColor(*legend_font_color) legend.font.color.brightness = legend_font_brightness - - # set category axis (vertical) properties + + # set category axis (vertical) properties category_axis = chart.category_axis category_axis.has_major_gridlines = caxis_has_major_gridlines - category_axis.has_minor_gridlines = caxis_has_minor_gridlines + category_axis.has_minor_gridlines = caxis_has_minor_gridlines category_axis.major_tick_mark = tick_mark_pos_dct[caxis_major_tick_mark] category_axis.minor_tick_mark = tick_mark_pos_dct[caxis_minor_tick_mark] - category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] - + category_axis.tick_label_position = tick_label_pos_dct[caxis_tick_label_position] + category_axis.visible = caxis_visible if caxis_visible: - caxis_tick_labels = category_axis.tick_labels + caxis_tick_labels = category_axis.tick_labels caxis_tick_labels.offset = caxis_tick_labels_offset caxis_tick_labels.font.name = caxis_tick_labels_font_name caxis_tick_labels.font.size = Pt(caxis_tick_labels_font_size) @@ -1269,8 +1272,8 @@ def add_stacked_bar_chart( caxis_tick_labels.font.italic = caxis_tick_labels_font_italic caxis_tick_labels.font.color.rgb = RGBColor(*caxis_tick_labels_font_color) - # set value axis (horizontal) properties - value_axis = chart.value_axis + # set value axis (horizontal) properties + value_axis = chart.value_axis value_axis.has_major_gridlines = vaxis_has_major_gridlines value_axis.has_minor_gridlines = vaxis_has_minor_gridlines value_axis.maximum_scale = vaxis_max_scale @@ -1279,9 +1282,9 @@ def add_stacked_bar_chart( value_axis.minor_unit = vaxis_minor_unit value_axis.major_tick_mark = tick_mark_pos_dct[vaxis_major_tick_mark] value_axis.minor_tick_mark = tick_mark_pos_dct[vaxis_minor_tick_mark] - value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] - - value_axis.visible = vaxis_visible + value_axis.tick_label_position = tick_label_pos_dct[vaxis_tick_label_position] + + value_axis.visible = vaxis_visible if vaxis_visible: vaxis_tick_labels = value_axis.tick_labels vaxis_tick_labels.font.bold = vaxis_tick_labels_font_bold @@ -1292,15 +1295,15 @@ def add_stacked_bar_chart( if vaxis_tick_labels_num_format is not None: vaxis_tick_labels.number_format = vaxis_tick_labels_num_format vaxis_tick_labels.number_format_is_linked = vaxis_tick_labels_num_format_is_linked - - # set plot area properties - plot = chart.plots[0] + + # set plot area properties + plot = chart.plots[0] plot.has_data_labels = plot_has_data_labels plot.gap_width = plot_gap_width plot.overlap = plot_overlap - + if plot_has_data_labels: - data_labels = plot.data_labels + data_labels = plot.data_labels data_labels.position = data_label_pos_dct[data_labels_position] data_labels.font.size = Pt(data_labels_font_size) data_labels.font.bold = data_labels_font_bold @@ -1310,10 +1313,10 @@ def add_stacked_bar_chart( if data_labels_num_format is not None: data_labels.number_format = data_labels_num_format data_labels.number_format_is_linked = data_labels_num_format_is_linked - - for i, ser in enumerate(dataframe.columns): - ser = plot.series[i] - ser.invert_if_negative = invert_series_color_if_negative + + for i, ser in enumerate(dataframe.columns): + ser = plot.series[i] + ser.invert_if_negative = invert_series_color_if_negative if series_line_color is not None and series_line_width is not None: ser.line.color.rgb = RGBColor(*series_line_color) @@ -1321,42 +1324,42 @@ def add_stacked_bar_chart( elif series_line_color is not None and series_line_width is None: ser.line.color.rgb = RGBColor(*series_line_color) - + # generate overlay axis labels if (caxis_visible == False) or (caxis_visible == True and str(tick_label_pos_dct[caxis_tick_label_position]) == "NONE (-4142)"): - + cht_plot_height = get_cht_plot_height(height) heightPerLabel = cht_plot_height/len(dataframe.index) rightofchart = left + width txtbx_width = width / 5 firstposition = top + get_upper_cht_plot_gap(height) - + cat_labels = dataframe.T.columns - + for i, label in enumerate(cat_labels): - - top = 0 + + top = 0 pointRelPos = len(cat_labels) - (i + 1) top = firstposition + pointRelPos * heightPerLabel - - add_textbox(slide, + + add_textbox(slide, text=label, left=142875, top=top, width=rightofchart - width, height=heightPerLabel, font_name=caxis_tick_labels_font_name, - font_size=caxis_tick_labels_font_size, + font_size=caxis_tick_labels_font_size, fit_text=False, - word_wrap=True, - font_bold=False, + word_wrap=True, + font_bold=False, font_color=caxis_tick_labels_font_color, - horizontal_alignment='right', + horizontal_alignment='right', vertical_alignment='middle') '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_table( - slide, - df, + slide, + df, question_text, left=4, top=8, width=5, height=8, margin_left=0.5, @@ -1371,7 +1374,7 @@ def add_table( side_member_font_bold=False, side_member_font_italic=False, side_member_font_color=(0,0,0), - side_member_font_para_alignment=PP_ALIGN.LEFT, + side_member_font_para_alignment=PP_ALIGN.LEFT, side_member_vert_alignment=MSO_ANCHOR.TOP, sidemember_shading=True, sidemember_shading_color='No fill', @@ -1381,7 +1384,7 @@ def add_table( top_member_font_bold=False, top_member_font_italic=False, top_member_font_color=(255,255,255), - top_member_font_para_alignment=PP_ALIGN.CENTER, + top_member_font_para_alignment=PP_ALIGN.CENTER, top_member_vert_alignment=MSO_ANCHOR.BOTTOM, top_member_shading=True, top_member_shading_color=(0,0,128), @@ -1391,7 +1394,7 @@ def add_table( values_font_bold=False, values_font_italic=False, values_font_color=(0,0,0), - values_font_para_alignment=PP_ALIGN.RIGHT, + values_font_para_alignment=PP_ALIGN.RIGHT, values_vert_alignment=MSO_ANCHOR.TOP, values_shading=True, values_shading_shading_color='No fill', @@ -1406,7 +1409,7 @@ def add_table( question_box_shading=True, question_box_shading_color=(0,0,128) ): - #------------------------------------------------------------------------- + #------------------------------------------------------------------------- left = Cm(left) top = Cm(top) @@ -1419,7 +1422,7 @@ def add_table( shapes = slide.shapes table = shapes.add_table(rows, cols, left, top, width, height).table - #isolate seperate sections of a table + #isolate seperate sections of a table row_labels = list(df.index) col_labels = list(df.columns) table_values = df.values @@ -1438,7 +1441,7 @@ def add_table( #row specific properties for idx, row_label in enumerate(row_labels): - + cell = table.cell(idx+1, 0) cell.vertical_anchor = side_member_vert_alignment @@ -1446,7 +1449,7 @@ def add_table( if sidemember_shading_color == "No fill": fill = cell.fill - fill.background() + fill.background() else: cfill = cell.fill cfill.solid() @@ -1458,24 +1461,24 @@ def add_table( paragraph.font.size = Pt(side_member_font_size) paragraph.font.name = side_member_font_name paragraph.font.color.rgb = RGBColor(*side_member_font_color) - paragraph.font.bold = side_member_font_bold + paragraph.font.bold = side_member_font_bold paragraph.font.italic = side_member_font_italic - paragraph.alignment = side_member_font_para_alignment - + paragraph.alignment = side_member_font_para_alignment + cell.text = row_label #add col labels for idx, col_label in enumerate(col_labels): - + table.columns[0].width = Emu(first_column_width) - + cell = table.cell(0, idx+1) cell.vertical_anchor = top_member_vert_alignment if top_member_shading: if top_member_shading_color == "No fill": fill = cell.fill - fill.background() + fill.background() else: cfill = cell.fill cfill.solid() @@ -1490,13 +1493,13 @@ def add_table( paragraph.font.italic = top_member_font_italic paragraph.font.color.rgb = RGBColor(*top_member_font_color) paragraph.alignment = top_member_font_para_alignment - + cell.text = col_label #add values for i, val in enumerate(table_values): for x, subval in enumerate(val): - + cell = table.cell(i+1, x+1) cell.vertical_anchor = values_vert_alignment @@ -1504,7 +1507,7 @@ def add_table( if values_shading: if values_shading_shading_color == "No fill": fill = cell.fill - fill.background() + fill.background() else: cfill = cell.fill cfill.solid() @@ -1521,14 +1524,14 @@ def add_table( cell.text = str(subval) - #add question label + #add question label cell = table.cell(0,0) - cell.vertical_anchor = question_box_vert_alignment + cell.vertical_anchor = question_box_vert_alignment if question_box_shading: if top_member_shading_color == "No fill": fill = cell.fill - fill.background() + fill.background() else: cfill = cell.fill cfill.solid() @@ -1549,52 +1552,52 @@ def add_table( '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' def add_picture( - slide, - img_path, + slide, + img_path, left=Emu(50000), top=Emu(50000), width=Emu(50000), height=Emu(50000) ): ''' - Add picture shape displaying image - + Add picture shape displaying image + Params: ------- slide: python-pptx slide object img_path: path to a file (a string) or a file-like object ''' - + # add picture to slide x, y, cx, cy = left, top, width, height slide.shapes.add_picture(img_path, x, y, cx, cy) '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' - + def chart_selector(slide, df, chart_type, *args, **kwargs): - + if chart_type == "bar": - add_bar_chart(slide, - df, - *args, + add_bar_chart(slide, + df, + *args, **kwargs) elif chart_type == "stacked_bar": - add_stacked_bar_chart(slide, - df, - *args, + add_stacked_bar_chart(slide, + df, + *args, **kwargs) elif chart_type == "column": - add_column_chart(slide, - df, - *args, + add_column_chart(slide, + df, + *args, **kwargs) elif chart_type == "pie": - add_pie_chart(slide, - df, - *args, + add_pie_chart(slide, + df, + *args, **kwargs) elif chart_type == "line": - add_line_chart(slide, - df, - *args, + add_line_chart(slide, + df, + *args, **kwargs) else: raise ValueError('chart type not found') \ No newline at end of file From bb06efc576f57c23563d77a37e75922835fb0a35 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Fri, 25 Aug 2017 17:23:07 +0200 Subject: [PATCH 042/733] catch array items by parent tag --- quantipy/core/builds/excel/excel_painter.py | 46 +++++++++++---------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/quantipy/core/builds/excel/excel_painter.py b/quantipy/core/builds/excel/excel_painter.py index 97417a17b..5c6b4fe0c 100644 --- a/quantipy/core/builds/excel/excel_painter.py +++ b/quantipy/core/builds/excel/excel_painter.py @@ -1676,27 +1676,26 @@ def ExcelPainter(path_excel, elif chain.orientation == 'y': x_keys = chain.content_of_axis for xk in x_keys: - as_mask = re.sub('\[.+?\]', '', xk) - if as_mask != xk: - if meta['masks'].get(as_mask): - if as_mask not in mask_label.keys(): - mask_text = meta['masks'][as_mask]['text'] - question_label = mask_text[text_key_chosen['x'][-1]] - write_question_label( - worksheet, - question_label, - formats['x_left_bold'], - current_position['x']-1, - col_index_origin-1, - formats_spec.row_height, - formats_spec.row_wrap_trigger, - formats_spec.format_label_row, - view_sizes) - current_position['x'] += 1 - for vk in coordmap['x'][xk]: - coordmap['x'][xk][vk][0] += 1 - coordmap['x'][xk][vk][1] += 1 - mask_label.update({as_mask: question_label}) + if meta['columns'].get(xk, {}).get('parent'): + as_mask = meta['columns'][xk]['parent'].keys()[0].split('@')[-1] + if as_mask not in mask_label.keys(): + mask_text = meta['masks'][as_mask]['text'] + question_label = mask_text[text_key_chosen['x'][-1]] + write_question_label( + worksheet, + question_label, + formats['x_left_bold'], + current_position['x']-1, + col_index_origin-1, + formats_spec.row_height, + formats_spec.row_wrap_trigger, + formats_spec.format_label_row, + view_sizes) + current_position['x'] += 1 + for vk in coordmap['x'][xk]: + coordmap['x'][xk][vk][0] += 1 + coordmap['x'][xk][vk][1] += 1 + mask_label.update({as_mask: question_label}) if dummy_tests: dummy_row_count = 0 @@ -2051,7 +2050,10 @@ def ExcelPainter(path_excel, question_label = df.index[0][0] existing_format = formats['x_left_bold'] if extract_mask_label: - as_mask = re.sub('\[.+?\]', '', xk) + if meta['columns'].get(xk, {}).get('parent'): + as_mask = meta['columns'][xk]['parent'].keys()[0].split('@')[-1] + else: + as_mask = re.sub('\[.+?\]', '', xk) if as_mask in mask_label.keys(): question_label = df.index[0][0].replace( '{} - '.format(mask_label[as_mask]), From 7e9d0adb38ca93f0f323e38665efb01f51c8de9b Mon Sep 17 00:00:00 2001 From: "YG\\alasdaire" Date: Thu, 14 Sep 2017 11:48:26 +0100 Subject: [PATCH 043/733] Bug fix for mask base label when elements have equal base sizes --- quantipy/core/builds/powerpoint/transformations.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quantipy/core/builds/powerpoint/transformations.py b/quantipy/core/builds/powerpoint/transformations.py index 95cd9f60d..f8f7395a6 100644 --- a/quantipy/core/builds/powerpoint/transformations.py +++ b/quantipy/core/builds/powerpoint/transformations.py @@ -450,9 +450,10 @@ def get_base(df, base_description): # if all_same(base_values[0]): # base_text = base_text_format(base_description, base_values[0][0]) # else: - it = zip(top_members, base_values[0]) - base_texts = ', '.join([base_text_format(x, y) for x, y in it]) - base_text = ' - '.join([base_description, base_texts]) + # it = zip(top_members, base_values[0]) + # base_texts = ', '.join([base_text_format(x, y) for x, y in it]) + # base_text = ' - '.join([base_description, base_texts]) + base_text = base_text_format(base_description, base_values[0][0]) return base_text From b3d7e075053993976411aea492055a2b0c609414 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Tue, 19 Sep 2017 09:30:03 +0200 Subject: [PATCH 044/733] differ between arrays and columns vars with multiple groups --- quantipy/core/builds/powerpoint/pptx_painter.py | 6 ++++-- quantipy/core/builds/powerpoint/transformations.py | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/quantipy/core/builds/powerpoint/pptx_painter.py b/quantipy/core/builds/powerpoint/pptx_painter.py index e6485db31..eb846bef6 100644 --- a/quantipy/core/builds/powerpoint/pptx_painter.py +++ b/quantipy/core/builds/powerpoint/pptx_painter.py @@ -766,7 +766,8 @@ def PowerPointPainter( else: base_text = get_base( df_grid_base, - base_description) + base_description, + True) if base_repr and ('Base' in base_text): base_text = base_text.replace('Base', base_repr) @@ -946,7 +947,8 @@ def PowerPointPainter( if not df_base.empty: base_text = get_base( df_base, - base_description) + base_description, + False) else: raise Exception('Base dataframe empty for "{}".'.format(downbreak)) diff --git a/quantipy/core/builds/powerpoint/transformations.py b/quantipy/core/builds/powerpoint/transformations.py index f8f7395a6..0e4924aa7 100644 --- a/quantipy/core/builds/powerpoint/transformations.py +++ b/quantipy/core/builds/powerpoint/transformations.py @@ -403,7 +403,7 @@ def validate_cluster_orientations(cluster): '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' -def get_base(df, base_description): +def get_base(df, base_description, is_mask): ''' Constructs base text for any kind of chart, single, multiple, grid. @@ -450,11 +450,12 @@ def get_base(df, base_description): # if all_same(base_values[0]): # base_text = base_text_format(base_description, base_values[0][0]) # else: - # it = zip(top_members, base_values[0]) - # base_texts = ', '.join([base_text_format(x, y) for x, y in it]) - # base_text = ' - '.join([base_description, base_texts]) - base_text = base_text_format(base_description, base_values[0][0]) - + if not is_mask: + it = zip(top_members, base_values[0]) + base_texts = ', '.join([base_text_format(x, y) for x, y in it]) + base_text = ' - '.join([base_description, base_texts]) + else: + base_text = base_text_format(base_description, base_values[0][0]) return base_text From 3fac683e9c8a0d8dd4102e1411fdf618591a2607 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 13 Oct 2017 15:25:32 +0200 Subject: [PATCH 045/733] New entry for RN --- docs/API/sites/release_notes/01_latest.rst | 34 ++------------ docs/API/sites/release_notes/02_archive.rst | 52 +++++++++++++++++++-- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/docs/API/sites/release_notes/01_latest.rst b/docs/API/sites/release_notes/01_latest.rst index 1d6e365d5..c857fe510 100644 --- a/docs/API/sites/release_notes/01_latest.rst +++ b/docs/API/sites/release_notes/01_latest.rst @@ -3,40 +3,12 @@ :includehidden: =================== -Latest (15/09/2017) +Latest (17/10/2017) =================== -**New**: ``DataSet.save()`` and ``DataSet.revert()`` +**Update**: ``quantipy.core.weights.Rim.Rake`` -These two new methods are useful in interactive sessions like **Ipython** or -**Jupyter** notebooks. ``save()`` will make a temporary (only im memory, not -written to disk) copy of the ``DataSet`` and store its current state. You can -then use ``revert()`` to rollback to that snapshot of the data at a later -stage (e.g. a complex recode operation went wrong, reloading from the physical files takes -too long...). -**New**: ``DataSet.by_type(types=None)`` -The ``by_type()`` method is replacing the soon to be deprecated implementation -of ``variables()`` (see below). It provides the same functionality -(``pd.DataFrame`` summary of variable types) as the latter. - -**Update**: ``DataSet.variables()`` absorbs ``list_variables()`` and ``variables_from_set()`` - -In conjunction with the addition of ``by_type()``, ``variables()`` is -replacing the related ``list_variables()`` and ``variables_from_set()`` methods in order to offer a unified solution for querying the ``DataSet``\'s (main) variable collection. - -**Update**: ``Batch.as_addition()`` - -The possibility to add multiple cell item iterations of one ``Batch`` definition -via that method has been reintroduced (it was working by accident in previous -versions with subtle side effects and then removed). Have fun! - -**Update**: ``Batch.add_open_ends()`` - -The method will now raise an ``Exception`` if called on a ``Batch`` that has -been added to a parent one via ``as_addition()`` to warn the user and prevent -errors at the build stage:: - - NotImplementedError: Cannot add open end DataFrames to as_addition()-Batches! +"""" diff --git a/docs/API/sites/release_notes/02_archive.rst b/docs/API/sites/release_notes/02_archive.rst index 7fd1b57e8..a6bcc9566 100644 --- a/docs/API/sites/release_notes/02_archive.rst +++ b/docs/API/sites/release_notes/02_archive.rst @@ -8,9 +8,55 @@ Archived release notes ====================== -------------------- -Latest (31/08/2017) -------------------- +--------------- +sd (15/09/2017) +--------------- + +**New**: ``DataSet.save()`` and ``DataSet.revert()`` + +These two new methods are useful in interactive sessions like **Ipython** or +**Jupyter** notebooks. ``save()`` will make a temporary (only im memory, not +written to disk) copy of the ``DataSet`` and store its current state. You can +then use ``revert()`` to rollback to that snapshot of the data at a later +stage (e.g. a complex recode operation went wrong, reloading from the physical files takes +too long...). + +"""" + +**New**: ``DataSet.by_type(types=None)`` + +The ``by_type()`` method is replacing the soon to be deprecated implementation +of ``variables()`` (see below). It provides the same functionality +(``pd.DataFrame`` summary of variable types) as the latter. + +"""" + +**Update**: ``DataSet.variables()`` absorbs ``list_variables()`` and ``variables_from_set()`` + +In conjunction with the addition of ``by_type()``, ``variables()`` is +replacing the related ``list_variables()`` and ``variables_from_set()`` methods in order to offer a unified solution for querying the ``DataSet``\'s (main) variable collection. + +"""" + +**Update**: ``Batch.as_addition()`` + +The possibility to add multiple cell item iterations of one ``Batch`` definition +via that method has been reintroduced (it was working by accident in previous +versions with subtle side effects and then removed). Have fun! + +"""" + +**Update**: ``Batch.add_open_ends()`` + +The method will now raise an ``Exception`` if called on a ``Batch`` that has +been added to a parent one via ``as_addition()`` to warn the user and prevent +errors at the build stage:: + + NotImplementedError: Cannot add open end DataFrames to as_addition()-Batches! + +--------------- +sd (31/08/2017) +--------------- **New**: ``DataSet.code_from_label(..., exact=True)`` From f4ccda28a23bceedb8d2f44a4177ccda7f5740f4 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 16 Oct 2017 11:35:18 +0200 Subject: [PATCH 046/733] Docs... --- docs/API/_build/doctrees/environment.pickle | Bin 111475 -> 113626 bytes .../doctrees/sites/api_ref/Cluster.doctree | Bin 21095 -> 21080 bytes .../doctrees/sites/api_ref/stack.doctree | Bin 144484 -> 144469 bytes docs/API/_build/html/genindex.html | 39 +++++++++++++----- docs/API/_build/html/index.html | 6 ++- docs/API/_build/html/objects.inv | Bin 2234 -> 2264 bytes docs/API/_build/html/search.html | 6 ++- docs/API/_build/html/searchindex.js | 2 +- docs/API/_build/html/sites/api_ref/Chain.html | 6 ++- .../_build/html/sites/api_ref/Cluster.html | 6 ++- .../html/sites/api_ref/QuantipyViews.html | 6 ++- .../_build/html/sites/api_ref/Rim_scheme.html | 6 ++- docs/API/_build/html/sites/api_ref/View.html | 6 ++- .../_build/html/sites/api_ref/ViewMapper.html | 6 ++- .../html/sites/api_ref/quantify_engine.html | 6 ++- docs/API/_build/html/sites/api_ref/stack.html | 6 ++- docs/API/sites/release_notes/01_latest.rst | 39 ++++++++++++++++++ docs/API/sites/release_notes/02_archive.rst | 2 +- 18 files changed, 110 insertions(+), 32 deletions(-) diff --git a/docs/API/_build/doctrees/environment.pickle b/docs/API/_build/doctrees/environment.pickle index fe076565707a58aa11e8fbea973d9c7e4daf680b..654e437748afbe55b930ddacdc94657a4e8e3f94 100644 GIT binary patch literal 113626 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 diff --git a/docs/API/_build/doctrees/sites/api_ref/Cluster.doctree b/docs/API/_build/doctrees/sites/api_ref/Cluster.doctree index e1206fb921beb9da078b8c4257805b95f9753472..14408817350e2a46d572132d752dc9afe56d3d02 100644 GIT binary patch delta 7616 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 delta 7791 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 S=U~bbG(N@9r!hu7E&m1f@=Ch^ diff --git a/docs/API/_build/doctrees/sites/api_ref/stack.doctree b/docs/API/_build/doctrees/sites/api_ref/stack.doctree index ca33de8297b3ca21a3cad98898624f769aa604f9..881c95fdc011dc3fd18826f52c66b4ad7b263902 100644 GIT binary patch delta 31865 zcmaJ~cVHC7|7BKM0x6dcN$7B)L+DjOv4J!R1_eV%4w54UHVH*I5k3?^3i84U;V7*8F#A7k4kh) zlA1KB2!{J5>6HPMpD`PU5w{d+K}ENsCKbIR?i{`Qc$J+~AjFQd+%n1U?Y`12CpDzi zI8~LYjxXAY7-KGy$=K#7Zfu=vLgUNs}fE8$#|<(GDl+ooOm>plFHRe94Pr z9JdMuXRmatkwx4Zq{gjaxFV@>eW#oewFX~6e4!5)#fCZIGmHx^@j4Gj)%AMH(W;lz z01w8h0X?k;i@*u5Hdc#iwZvG38<307OHHUEDK4KTd2#V!2&=B%b(i4~4lhR*aU)2r z!;6y|w@&Un`g8&-e1P#35=|k|p?B{N?ioO#v69p?{ZzgB7&SHHEQ7Biezw8&+GEuC zgmZ9!-Yk#y=4vw57>4Q1bK`sSJTZ{oJfGAH^ySB@OVouVbrN$4ajgj0-n@v!by8H( zP475PW$H7IQ%#-qrYjRfNQ^e~MdAb9uBAZQWOZXFG8VEw40|;meUrCtk$}2Z*dVyH#@219NI? ztK6$`46m<27ICjdYI|`@Qe)}j4sEU@aJ>PF9dul;4kCL4W&2q_ZUh0c26u155k$Aq z_GV+trnpYe9;>o5ZXvMU1E6;+W7H{*(@IAss20v`|B7@6*f!Fgw7=chhi^kJF5N+w z5=2}gy;E0BP&rNS!lB+ZL1j8S&A7Z51rhf?q&8I30se7$KTrhl0I3i9sT)sFmB|ko z_+i4k^x_lLyhQgA9HF<*PtqM-H6mj-!AFsrYT)2wddd{lKJ#%Y3(qiT-X7APKx2QM zku5eA1>rYaBkK{K35LY=q+UN!^;opvaYc`! z=D4`j*35I?(WgvSMalb(#k;h4Pd_|aO?UPt>8kxInDIUf4#=gel(x!;prAUe`)KOnc4@*ipNlQA&Yw4cqDVBHH%8(Viy)n86k z{S!r3On*sg+}ycs?yoq6@4wObch5K&{xPX>%9bH5{~+*ZQscTUS?Yg!=ZWaMzx3V{ z)d=TrFy75<`41UQk{fH@&B{>85`E>l6=9y-3zoW0vR=1DbuBmyNJVv6vWAL56DtN+ zIO0ZA8q{KVG{avJx4GUrP0e@yNow3tesY(&Zf$i*UFjmXg;@8fsFOBmNs|Gy+yp7i z(1D9pdWS^(jW}8%i@2?k&Z#JzRnccwMc-K!182EOdRLL^Gcp-KLB&ecmO^4GkGmDM zsH-onuPp1}22g;rQhY(LM{8XrwWjHPGgM|{ho2JXrweLMEo+y7B2=G=EaGM%wL?Ja z;Si{3M6KDxa|~YG5~JNlcu=JqrP4NDrDNN|2$lM6++38Q);y}_8&zI7r5;o3<*2nC z@%B37By~zgd}vonEgigC(mGP)Y8Z8*QRig6Fw4nOU4)XIAWG6}U9~Dv>4n{Ji28%1 z79h1zi!o2nifs*ZDnzb3IX!g0VwJ6WQf;DD>jlh)R#_FQDXk8 zcnn`*2oyqlB<)5awc4T-uRWTa!}ZNGR9AIGd;^cs-_KATPZ$dd)Q%+c zD5O?RY~$6&k#n@+@IqQyTU0-H=%{IKLr8l0nD`ow)hkX`S-Ho-05u#>>+!~#Lug#` zh7Yp3Ooc#7v}$C*M5Pcyc&XnU$>3wKJ`TKS?X1AaE?(VK87>2s1B4}6@REw)~J zcsJoPQ&Zo!S~%s&dP-1bj1GZ_Xy(#*9#Y$vB4+fq{Ya+ zsqYdJmzryZb6PUjE(u~T)MdO_t{pw}4p(6dLPE-QcI{duOX7 zG8zqjG4V?bu6NB=L!094sn`3$s$IrLg4%%QK7VO^2W|q!4!n%i&HC0-HC$azQl6Mg zbXSOgUAt@{@k(BuovTd$4H0W$Zk0TG=dqXPt5JqPuA$wvNNr#Nai_WBvC!4&nbB$Rh5WNUi@bo9=vz zz}r5+IPUMxcYs1;KdJBfse0HvH7NN#gTGJwfL=aN4Uq19gx#rMo2PoJs(YLYRj(X# zskvu!rRuHp^I_E~qh*SCmVnfFhKLgN%K6wYvX|Lbz}VZZNlns)^VLL^Owu4Rm#|Yr z!1i`3i2*4>Z_7-uvUWmkS!oq}JS~Nn-60|;rsVj!H9&#P`bS%<1dsNB#2Iu z$GW{U4&i?nWD&P3QtN;F6kdb75eWJK}zE_3Yqtzxa;r4~!jKNa_IHSfe6pAV~wnTml;;0=8oZlQ=|*(6P9ZXkV83U^?{_TaW&Eaq%$XGuz<>XkWqDR^-4 zG}_Kt*Srv{-eG-YF79S1ecS*^5<&+%-6a@8{C?JkK^5UX{XKnDYr- zkiuP<3-$Mlu?w?Sr!G-LoQuG~F3dVI)~CoWO!@{Y_;+C%^+iimw}OiSU=v0%|0c{O z)-FZ!|b3N>#O`W||ZNJ<{&i#g?SAMLrIgTG-HHo&G2UB$9 zW0hC<5O@%Nm`1yd5yKRjc;QFL*{!!NSM4!Xl^IU5m3s_WMX}zoTxI6DkK+KEd&qwR zsde^|6xnEalI2hN%4I~yh5|Plo{qKKGi1st^;udzr!R=8{wdFkQ7|lOaU(bB1u2Pc zHM~d}v(>OypLn|JH+3Iic=r;ri2E{9co&X}Nd@d%yVdXtS+5!v+r`x9-)eZxbG75^ zLbO{AZ_xHl&lcC7x7cd=qAGp;HtEr=hIdHcj|&L48pPIX5pS#ET~m{6HN2N%wi@0C z5z!o=@drq4TZ^0tCA!t{At@hu6u9#--6>#YaOV?XcB|o2vOn{)KPS7p=!o-Q2xhk$ zz9juCKmBXc+lY|RzmZ%z@mo^AOVN|RRCVfmvhu`USU({5=1M=3`4dkQ=VvoH8Ybq# z{6+GE&XZqJistx@mcJvl&G92ANO}X#xXvOGn+*L$?%y64?eY(2xPd0A@pGX@y>-3n z($jjN0Nc5cgTEv0VMt{(iJ{zTo9ca+s>}hwu+$+=HxnQ>$8C;7c-sOQ_PUW;Zyh~m zjp}lE0)a#yU|f%0r}&_>6;NojCN(LJs=xnQ%}Pl&gcK4|Q}wKG)Tz$5(J5|#rD>iS zoKC0HZG&Oka~VL{bD1P(>9SR7p~@z0crYvy5?hXl*lBJXQrk)q<{wSmfg7r; z!?PY~n%q<_VTOyQSm#M@o(lPxZa#_;T{~nEw>?tZLTyrI6{AbeMt_P%tUo)G+r{Ie zvaYFeS8nXLq-on_!Fi`uE(k{4ZXoz4y3x+H{saNr&Mlx~ccT~GS<9A^-a~KV^vn>U?M|21WpfSOXy{*ReE3;fd6QL;ec%mjG)p;qZAfHX@ODn zuT?NC6-f(>*2xH}?cq3tV@Dv1xMPsophs|6j~h$iNFQK&&_Aq?0t$_Bq#k_;s{Y>^ zH7dnD#_*0M?>PO-8g;ajnQGQ=$Fp#}XAGAo&}HA?J^?5P_e7E>=`H7~`D!w0?Sf&E zkjSQph#lNhNj*`II#0E6rkQ~}OzeeOB)PdR$vmQ@thB5mzhTE3m!*Nhuv8&F zmrL$jq=j${Z|5S5xbu)&Z%cinwt{G-L4BhZ``6~8XC0akN^I0t;RxE*w5~DM9JLkE zQCmx7frr51g{k`c4AobkK0!4PxIq3R{5rrk{CY|?7^$!rO8D5`hJ&qxVW~;NU!1C| zC#dYUOK=R=mLiL|ry;dr*K^#CUPfTK4=_FEAGZ;p&^Vpc6@IFI?m{(A-+m$1OkLNi zBbIxNL}So;o;fzuzfIWCkp2XYL`r_X|N`h%TuvFc%p#q?MqcwW52hWPdQHr z@ypZmQ3M|^pw5LzZJeSUH)mK&%0(UpzOAEg!C;s*qxIGU#u+b&W)z1h#D!n8qcqVZ*^TnAo)gM)+G>;>gqp2~IL72qL|EwsH7sSQNb;sUvf zl&u~Gfm}`B`UJzQ4S`$(l!07J@^yyH6@)xXl%aDyZF&a7q9;S=2IO`*cq7ejGG-Xk z;BS-5ZBVaUuTqt{!3+>_vAkJwbI$Qeicxeq_z;;`4nC|~UySA8E*!)4M`*SiY5a2VQGL|Ks#oAK zTFImF$4T6yS6-|#GNdx}byT0LvL~dK+aK|yAZGi^4V5>Ph1{o5f{LD|{WC~y8;K(O zyF%7v&k}!5x4cA6|Cc`3WY1H-?+X-=_1KFv*vsQ?^7OGud1-xV$^6jLIzH-^uH472 z$IVXEr2}8m?_Z+Y6uyi@`1A@TUNsWDo{1=YLZjISU#|p}06}%5L+MY|y6YK*h zf%FG7`w*#>7NL0QkI4DhaJVJ8)N7hgSW{K3rcXhO)?`@jXDC5UpHt-vqrz4a-T0cm zBH{0tNk$SQnMWoU7{g5gP+uvFKhp$}D$k^cK!uC(8s(tqV z$ddKYU$pw$Sn2OJsR=Fr5wl?Ucbo*mAI*80Fz zDmzaU*m`Z?EoE^5Lm+xbp=#y?0%k3n1|~vDr*#HW+q|M@ z;)t$gGfBzvDDWqn{^YPW_>%*S7xOlxw$(3gRs&ToN%>+fPUne$oz>-&*e=lc&zMZ7 zeSpi8L1Hb;4w9R@X!PWCJE92QcA{Blq}JPf{rwh|-KGnHt_JYUkGj!^epWdM0+ge3 z`T`t5v^#Bk7+dyJ7bDt}KraIn<1v4eC311+E z2JrbfAl}D;AjEtegd>O!rtJ`8OVR#DbSQyg2A~go&w2R2d>jEn)W@Ze8%e8C#)@)7 zjofGgha14>;}P*bjsYR&<5(O)^hnwsWo#)rnxgsR2pnwyzH80XT-x#&hp9QQaBK>K(~j5iY09v9$~wFw44f%rrpE{d`F{UqX(Jsed}2{f);6x82csS?ws z0`O0B?@8q=4%Vv^0o!TrG%6JtrLY)E9Mjo8L!}xS!6!*>UXSu_Zf-FO5y}i?5%*-I zb_`Do$UD9{rx2KF0Cr_Bk)B2Ap4OLBfk%6?7t~5vEF$Ne0H@)rK|~a5XnZbG8->WlMR6V}=X(@HaRFWH#mW%Hg}@la zT2e1E)VN9bI$CrWEg9JBk=rTw1{yUQBMd$0Un~R6Ou?fAY=B6M)3iW2@Q_||}#f?o{; zQ*g=rQ}AnOAXD&b@mIvXPXB$q3Q-hW+{NbM%y!i>>w2+|&c<(`5oY5z>isuhHhvS1 z5ydtt+>A7SHhzoFyixTYzMWPw8^4vr+j!iKFOlQDD(uu%U(_HbeecPv5R4_x}Va9Qzbm#Qh8@ z91BOaL?4mjJ|VSzPRbYh_3i50+%Iw9)fV{lB8M#BtI zLA&`q`kg^HPmnX>58e89Rh054Sn%q9wEQce7u>Fh^gzDeK{t4*qDOdwF z(Sdta_6nhhj7mdpPVivaoYJ~b&~;D*$-|K0*#=VE!y?4-5?%#m-gKk$*=EM1IZav^ z6FY|shU497DI~{D&};8dZB=5tJFNg(cU{|=+N$@7?V_*1dPf5 z=T4fC96Sygb0?PT`1Sy;GtstACP26&FJ=YHY8z_m>w<0;%HU77*tt1Ktv{l4NPqN2 zcdE=G|DOYS6v{V3tZMe`U|HAT?AbCz+Qqj|d(hyJycZ}Ow*!vhOh;;TG8()RiRz)8 zNpw3KlP)yrYD|o8i8v49`>GpB!L)2fX`OSea|Q9vbqCuz2aekV$8fGEMS3B%EhVyt za;}5h+nDsBNnc~~|214clKQ9VDR=WTfw*x!03dvkH?M-#q0*Y5I}j!CW)Q6gBemX$ z&Y`^-VoZk8WSBAe|GXJak{Y4aJs8#_*)oIeI2r{I-pHq0!H^l&qfrER4yV}>NUb}= z(;5SRw3#%9z*qxtPTt4so)FxPt@e*3^C+K$x#svEUXRoJ?!jvRXd&6v{xP&W*0aNC zJC1JWi=tc{jwe03+8m~4O8Sy z7}3T4R8mg#C~#*Q-6>#YaHj~EeQ-UU?34WLVzRr7jyOLMcl0!w{>aU2rf`|_s&g~l>c zm;0&u?*~-BQ^UP1ho|?Xbr5<3$Ev zM|`~x7vWexZXn+1;Rxqq(~p;+>=6BE{n`lF_M@iArA8zyhSHClOg~Bu63Ate$NF(I z4&ndh$Rh3)NUi@mji3B(A#kMu?DY<+S5dXUbz>_CP>oIjuEr5Wuc7U=#+G-YEoo*~ z;5s7Ldk6$?z{Lcw(Y1O+rKH^m;6Gx%39yZM8YlsRQXSZh zX~ey|RlTz+I&kkJ^?r{EZy%ty-qq(pU>vv)k@~P+`zRjD>>_Efm`g~Hh=3iqyGeXh zity@%>|o0nT^y>MS5Ys`@fgSQFwqm+$0aw-5#zagP>N8VK!(qZk=o|i&5^8^J%;x# zL?Sk7pC$J>kBh3FH>36il>Ms*$L#4MlK^qxMdY>v_u_9nYC($n!eS>Lzr<(@r2;W{ zS@PJReFcYb^;KjM_cf%})xDE7+^bb(I>nS|l`f%gZYC*D-PR+xR$=g<^KdlxzA4dD784H^uH5I|J z7JB8Y_$*^ORK&5Cz}PPdq$cVq!UE0XfWTu7}&!1hZLiOG8Ovntz3NjG0%h_x_N zC69GXb*R474WJA`r_nAQsSP?Qo$oJQ^eoOJ5r}n6Hn}+-7k;-%=jHIx=TvT5TL6dX z9qU9cVB0%+RLVC>VKJ26X~*~yq#E(My?*dH)iJjN4&i7=WD&O$QtN15I&a~f33TxR z#%q7?bOj2HZlnhNR6XN)wIQRx;N6M$Fu0!mygE-+~>0A~-#eI)jkfz(qN39y*wmAsz*7Ih3At zWxZ(2VZb<`hLbwNPaR3BGdP`Fu=`%b7VMGHad?FAY(QgZIyRkKu%mEra6m_T0Xavd za|d=DcnIQX+8%?{1|e#3K^#lUaUKOh98b@Bu`UEL9vFj|K+lVfjx;paXNQk zXQZ1Q*pv0eFX8R@Q$WEE>`YQ-rE>@NR7&`FU`x`?4(x0o*nySIzXMxJ1KEL{gTLfe zsZ(B7Jt>rliUNy3%5z0O_e%klg<}BUblHGG*O$CN0m>BVSXKQdWsk zFf3|u9g=jmltlLc&!LQY>wC2>cwJ4Lx&|=ZI~Q5RJr60|3rEGI0&|i?ACsR?)&+*e zxvHtpzi__LbG74IA=<_BMYLV#+2RCS&j!mERcWvdq(>Lejig_UQv`2)i>=op-s1TZ zQ)y)oS^o;8Sw=29AOLTVdZXka-O?8~Q z%J?ki;`3G!uy?_$NxX)m!MWDl1rx+tnAdSQYdV*-x1r2`A1q1dlGeJh1F*dh?xe`=MkFkTav!{d5f(}f;`g1B z$7Ypx;Sm1cjV$8cgVg%J6Stbh9dhp_aGwFp5}?-YAiDQccYt-|0Z^isxDH^r58@cY z57GW%W6vRWZ@O6y?IQAshd^?-x&J@XN2R7c3gAEDehjdU`*AAmF-l=E6#t%}e~D6! z#Qh|OR9}^u`xK7h*VD-GnJ`it@Z+2S6g*4dIUitp%El%?+Vwn8z4skV&$j5me$_R3 zFA5;KkG3!AqxP$AfadR41sN~1>=mS@B6#+y8FThLc}*~TTYR1LH_UxrY!5z9-V}3l zpMOgT@%Q<+Q3O}sq0WA!Ha<~~J7wM_;N#HG9Qrop`ZE@sm{CR zKEL^0+~;jTp9#+f^f^txF!%Y7ad2=zUwQ#KUzz*-*We+DZ)p21QX7P*#Rc&lDc^e( z1n~ns>&3bd#E-xj#80IDY^ZVf`Cn+!U9@DV{EFP(=YONo@5ad7=l}5C=grwTK%~X; zPsx4v`TyYO70xzD!*g8RH={`-6a4dgzbh`%CkE8Xq`wM;eDD?d}Svs#N$^yZ#KCT{M@ zI`u<5UQNL@gqke+`^<{^m(){-s7`$ z|1Ypjy6)G6(byi`2H5=(Fc}bI&E7r<;GIYBS<%;LSbXt0Ao& zMdSf%dm44f(4$vkzr3T~`i07#E=tmMog_E=1HSLkI-?ZsbfINeq}CnLf;(tE`_^MO za)O2v`yQ=;)g;(zy6fd1<2MX?fCk;3wCQDRcy$z&c-`LQ^wH0LjL*^fQZ3o4^#g`S zbNfEV*Eeo|96+;>`~gU(nu-bl$6dK5wPgI0xl`xmO!iPnkH zfZ&8Y%Z)R04+j7bjzETQE+`l*hL7Wkm^4|AqLaEQVryP~oT^hkSAC|Q2ox?&qkR!lxD<{Gh$zCwOd>j`n@-kA zhQ*dS4u8kL`dI9_*Kvjr?VRpp+MeRsqK{^>-SS0M&XQTAN9S~>l3tR*Ii1*gt>De+ zW=le|k(AM}9I0(wkuqUKABKcTnd?#D$~?MKz>45X1u*+Cq>}8gpFN-K z?xG`(R|#exhE$VYo+Cm+Um&@3+CoxYJ?TsQC`O%W3^5l{J#ufd*g)c<=*4BR z4t%BZhl#Z?mq>ol#bqf<(Hf`Gav4(F8VwoTG@0-fHcdn#HcwnZ?in5z&9X8>=81tb zQRbg7wa~j;I9+;L56%K?=S!<7a<&l(i=mv2=PTJqRDaSaaP>$%9Vu0U#i zU8N8Fpt=;CPv8O{U>t{pw{LtQP<`9im^Llc-+ir4OQ?;CX$ATxrA|<2-pezW)d%#qC5j>yFG!xPbmM7z<&m81OJ6WzZ#*i7)s#3 z(YZvaMFRglqjATcthRsP5MKR>4383!+IW941NScifBOK_RsMnd4^U_{$rKHR)N1V2 zgMU*;COaqsei-qldi8JWC}(eU;5K7nbI%Blw$S_j#G{2usEVU4fpOp_keaA_|BmO) ztw_ofb0M`B0XuM$NKDp$|BiRD@Ns`LCx4h23o%u4`^eHxJ>39G;b9sL(~;T;k}~Bs zIU>X0nZ&bvxX8xFZ8q^74@WR<^zJ|Kp=n!`9isQFSGj;~@8wY>--v|8P+k>QyGQX4>CCLcF$bQ{BAgw!RG z7fNnk7>GV|HuVfZIh-AcEaDDAYFnUhCZDhu3n zOZ39Ouz=3~Th%!UnPv(xn$*KRD*VN7lKO}57+@T}V@W+yfAzPz0BZ#y4G?n)YMcnz z;d?ZR$4C*byYfC$bbTVNaV&@PU=b6;<0Ln`Gf|RzJcwxGqdAuKpa0+i zrr5{E?PPMNcwE#o)r{K{QFe&lv(--nYW1ZEk)H*Qa*Y=7%V2?&slP7-G02%@F5onve{Zf8cvZ5fer4}qT{ zj$8R@^t8ZS0RIv6Jis>S3My3^rLY)E(D-f}y_+D_NYGd(I8IJ(6%OH6H8Q+Sgw)1b zVMb^zfdxLm^pbysE(8h{wkv;u%teu`2Iynja#c!*6_7awxS73Ru&j=@(GU*p3?G&X|IC4QcVBewI+2)+Pihv-b} z(}jR-XRf8lMMfknhSHh%N?T4BRFS&07Z@@7EXhep0i;&s?)|wG~34x6U@Qq+i z+5Xm#OF@W@;7vG!=w-CsY-~A#FNu!e%ZXg!A@FmHZv1n2QX97@$L%RRK+1z21)e=b&$_Z+H08su78 znS8MQMWWNw3uuq;=Xk_L2IMp~h`^zf6k)(UKAL3Ua&Q{VI)KGe#KJ(8oVE_(K(# zulvom!T^yL%Qqx9Cmg@-e-p=u<1J(n_idy$j#o4JVEY{c`!l)W{cfh&@P1EkPjZGk z?}LH|+XqPbAX7HH(?6ty|H1Z0x^J@6q2OZx*zcCizu)}{`Lf^rDgMG|5PEU4b38Ti z7`24YwewS)l-$q7KDzb&1&y%v{iPHJ%yaFp@HZm)8d=2s25J2M_qY1A6lY-II{@an z_V*c^OhUx;*=Y^z<&vItNRa6IPFi0$X56NX!KX6c{TTMcK1Y4l1}?a^5~^L`u)G#B#X`{WD(av zYMl`!IOE&sK8&2EdVIjiSIzY90w+7!N;e0F4e2QXCw-vX0tX1NCHVHUnVWH^sQ;m$dqgxU}IL);zmqTj+8|AXt8jBjCt_hR-c#djF_4W81CgEi@5nn z;a)f@CKZ^IIJ!yOj;!{E#e0XT&;Q`PgXe0;jzYAXw4G?%*|Wt7+Jz04FRIeNT}h8_ z(sm;~n8i(6vGrQS+oUZpHOYhb?pbD|wg-rarYDViA+?Pyawe4MMs06W`gjz$)0gfP zurj#Q518Gk?N4^0pFM!=?xG{k4;0L9)D9wju%AAJ^c)cq`cTQG6NiyHT)&>_gzyo9 zjLJMQ7t%=N-mGX8iKF#`EGOGJJj=Y>Cf33{Lh^r|8e>p~wirvhBazy+7?s6s+TB@B zo6aH-oADe&?y(*hjdENTALk#BGXKs0qf9=|xAl()Y;XP(C~|@k35%gzj3+X*La9L< zo+No}n|3k|;p-G+SSBE~zD~&ELhD2V(|mw&JbDf1LaPWUG^Ue!lAo$WInI=fVuQ~h zezL)JRgN=JUT+(gW!7FZiOw>pxs99}-*Y9v*mJWVy?QiS=e{NQVCP1Vwp`lSmvp`B~`a;WHu?L5iNm*5`Htw14yt3-yS08-mPmSr|@R}wkXL*VII47(k_?3s&S0)ua>fd5DIX9Kp;pF_dbMldXf z68#!82&6WN{#?n;q%Rn);ye_=)ANzx(`uwP{BunB7ZO-&0AKhQ#fQHRgjo3NaRku~ zv~4uD4FAGt_!kqo#6#ffMuv}{+-s&MwR2jgX@LI``=x+w?3<`|nNbUip~SwK9uASJ zB=*ZCHy5N}U1iO@Dtw=K1;IA^lXWT&KMh}6nH|ZViouO&lQ1%}&-wfEsd<#Xk8jYf3=adg&9IAtj~KzQ7)mqjHeD~Z zNi#eux$XLB9gm?5u0D<|;_g9eznNNpF1a@^GEQ&K+j zDDdoadX~d_F$BH<#(U(Kq<*D8>gsG$`2T9SM-CEm87JR}fc;VQZ%O=4ilX<(9zEm+ zS!aF}9sg_%rlUkoOn;Ev{N@Wu?vE&d^FJZO*V#y|^P$fse=qZNNc{&<=7RqxMprjN;8iFZ<=Q}mIkwOSQnb11u!;qOHvc`@v?tAaAryw5nhg}NC2m;txG~hZgK$_-R$g1yP+wV9*9ZSAtfoFxQ=T}zw5lOgm*@@# zUh57)iZ3mMTa>2H?(L-Gzq4)X?c`<-N7=-p*6!T068Utxq`|FnN9d<}JDpaH1XPrO zfXnbzb=iD(6pq&vB`*jq5NAVZnleN`ngYpkqgqgcpB=77i`BV@voxu`R%FZOqXp;J zxkrd#GvMx0cZ~dK)sCq#a;&V@wI$8qj-L>Dq za(tEj9^|_fiBhwo=4G`tb1Ub$)2YBl#(-EjDm>0q+Un-4Uje?0q$!trkjzVrtX;sfUw}!>7>#A{bmn>|+wXDvq zMcKJUt)-v6b7_I@S?F}@ybzFF$7=P=o=S#OSOInA>|-BQecx%#1pZC9ixm2iYRtb z?1~M0N3rYIhQ0S*f1mf6o4q~Z=RY&q`F!4I-kUeOH#@s`dF9iD4?j&f@piRwf4zBs z)hk$$+^${3osY7pwqIGh+JOeEj}+dp^Buqyh7^o@_i6ibBpU9heJ5L5>>=K9;J17X>!}< znY|}Q2%O*pjHi%j4~c?7g9_YLK%sFWsjK}|eb#|$cHks~pG^D|{pEpbX8YR0aJByK zFqNgZ{h$(prxH8OvxVQM$C8+{1^|<|mb^0zulzJUYn;l^4;`eAQ)iN%CqfeDSwgWH zJ)2hN=%#~J59eHy!&H$H=6O<=m#0!I&s~Q@1am%3+ECg7Ioo9L0)t;je7z4By;ue{ z@ryhh0bQKjw!5~l?ea%DIG04TXnoiKp3UM$T3%`_!2_2R_YRac(3{PCy_3LQKEUK9I#`^qcLRmS zJ)~~;Q+3i|sxEM^!S5q}zg~TqS}5~%t<2Z)!3WscZl;nG9Qw*@KNSue3-OH zF#KNyv&E*iDEvkjRK?+A!H}3A6|+pe_;uB3)?>mbFYknaXNOu^-Nt73aWuirCn)qJ zN*k(3w^mhRvQ?SBo+9OGj{@yyIG#D|7~}aYFrGlqk-AflpP{PM^CTsTxeWRX`oS40 zFR%-TxD38X=1cms8LGQ_napWoEzDP>E-&xQ^fXtyb&Ff-ni}0#(F~7Yqvh);t;eDT zk86v|I^v?!)VR=nLx*RovcQ|h;(xSwOFuVL9pOBetmnS13WIO6@g0;#22s5$ZuH1} zPulE2?6x%*;SLA11e{fteFO0uio$Oyf^bT31CbZg4*(IXV>>~mFV4e z-p=-IU3#bL-)E~~Nun#JKP0zp=-n;%M;yZUpJ@EEXIvQmHo0xqWup>)A@FN*+j*B| zIlm?A-91#k`dz34fCESF*nOe3cdan;La%Yr{PC7q4*tN!F{EtG-3; z0_eFRELCgmi0QQ>YaMY!;kc-_3A4n(UlDg7y={(K`1OQi&6zOr<=7)Lle4BM-_21P}&(G{cuLqwxQP`@l1m=02}TeovxQov%F5HWWxxZ zO3>EE%|RP_?Ml^dMwQoC>BscC5xwRT&(pn+RL2D4L)#?1SU>&Ldl*kN|M4}QX6M#DqAWm7KK*0z0n3|`p~W~N;?Fi#PhfI`qrvi(e6i1q29M# zWvili>HffMoOSh~#>$3}TZ}eH51`pVlvY}V;-v?XGg#kOu5#26O4nMihXR9iB8E_8 zCroc@47tP52<;Num7=uTq7<(^oSYGcLz%^uEwz-9VI0ZcX4u|Fffwz~qq(Eeh~CCf zX{=FV#zZ^5w{hf**V~U$eboeNCtB@^>yK7F58D?Eh)p7KGD_>Zn8u4uA!k3s;ib2( zsjOwe=m~S&){r>Ae|!g1^*KkYtlR@&fF2H{^+Cp(?oJ8lw1~>;cQBDde282eVyDg> zqCHKtGj+kIsa5_t>(KIW#IkGUhrI?}KhgZN%sG##I*v!b7=T z0X?fw1t(U4iEyfEU4zo5Q}j$M(U0s9DGNLb{8>nUir5?csRb6!G~U;d9rm*qk=%_QZExsX~=du!-c5|^5biL)%g z8=;Y6F4W~x|Lbb90&N(K<7l`Nr5%h`y{<}i9e2FJBg9Ye;UXGaNv|S)qK9LoR_k>Y zYGC?FXe&!}Lknt}8Y=4=!~Z0=Jv6cR0PEAqfbF956pEZ`M8aYy6Y@01TOvJ35T{EW zyER&aL-@ZIRm435rS<=m0B?=XByg4wFpfjQyEQr+C^XI?^;|zyKQUiT3Z7^1b;Qp% zxZXKmP3VAgX-Pn@PE+~83y5B5P|SOHzCJ#k8W_`g5vdpJyDQaLbqPs%VlLrr5CMBj zw2{P1dHr@Si)K@-g}F)U=m(X(fM1R_M6#K7SD>_`vC(AnN`r49ew7aw(O5RGCVq{F zBbIATHm^h5Ub1O@x*o92<_#3N(TIe_P{w4d$)@xmLEI#DESopu5dLpNg}V)u*8dw! zHg6?xn-4IK`?GmFP-xsi>YaY7&aF}X0(TkwZsPao88xa;lDi#8NajV+WZp~gK9t4* zIDUV8G9M5FIX539b%%bvMom=@nPiH&ME9@=*knFJ;-h+2NM$&WnPirTwJ;x-+Du7) z5Hx~4;$sw?Y5ZYJ6gau)5fQQBck)0>-cHO?`3SK{4#xQNEmo=ZH> z!;!9h{oQI@jk}|5FKM?v^#E+sUOFwPiUzaNkWH{Izq2)R1r0jq*5`L2uF#4P3UM6$4C=)9Xn!yYnFF1GGk4C zhl`jPj+46FM{>ub1(8ia6>%q`w4*cHWOrYKPa;0qhl^w^yHkko=i%skf5~p2wu0@6 z=~L0Zmke944ghR3d?1AmGD2Z7lsFDH@k=if#vxMsHnFDR7(pD0D&ii7(gty$N%3?7 zGkk#Y-k;)`K%p^<)Y*Qj9<)S-1BV-Y4)HR5)e=>k=pKP1q>fqO zy`|#Og4z9tV@N+Xg&RF$yXR7Iu9!zRdgciuexs)XP4Ia>g(^|nxJ5c{)mTMJwMW62 z*3h*ac8oC%0prA5KngG&t4C9~Nub$yFEO4d3H zQp~1L3(;1CnzhbSdf*NIGQsTNFDHFP3O9emcF(~-PRyg5KPywX`Exvo@Hs-`6Hr>8 zMJ{f@SCMj}N5O!sraRr)83yDeU>xw1Nj=3-^H}3wNN_vq9$@ray+V1|GPK!0h z!o-R_Mx1T>@1?45^fM(@ck^>p6%HdhEGr7=Fd3<&Q0Ow&v_|k z^Jkq_%hf37d{D6Y(?-e#DctI;*0>wQG`jJ#k$i0YT&mAqp>hf?!!ZKbMB~d*#&7d%)=#WZ z#idu!O162fBykIm`-KDMLs+^-s-yjIcjqeRS?>H6UM*<+?#?x6La*1Nin!OIv?&*P zrkuMw*Au_N;9SN_JeZ&H9dc*qMz6z(TVW8r3NikhaE#t=rrI{6#v4}AH@(TO&Mm}m z)ty$ViNW|DxT|xU*F)Ov6p@{mJ7{#L9=uw0Rd;cA%@QG*BX>((UY>-LST-yw%qe;g zTH(!h8s3Z2#x7DkMZImE`^dRJMGyQ)6{rWOo@CV@Owo-Wsl1XMAVKdTavw%%^~5?} z?-6ny)faw zz+H-GW8?K4neqv|la|lxNJI@wd_jx~!%v&Zn$BeHlD6m;#fwzX+@g3%?|*_CI{Rh7 z@az@JyowT@g`+y6&2-u=ir2_`-LRN(aXfyD;tkKso^J}#Zc+S?wr_d1xYoSQ5z7}< zN$)$PN4F^6CH=h=Zc&J>HyGX)#cmS~wsN>Vg4rd9_PmIXvJ{+LCZf;+Tr-w zES&x__}|3;@!=vHTR62#mFtQ^Y3wmpj{fcpT*{ySQYEDC1Hiv>TA#w53hP>Xz;@-- zfl3{XQdkV-Q@E2>XQ}KG=|*BnkUF+DPQ)RCNJ15HJEOEgbV!x8u@0WB(gzxcqgN?D z2MZXp6q=>_%)Zx!C#!iWX$DUxo{_4jf34=J?|tjwAdyVZ5{_ok(Hxt^Y+y`c4yj%B zoKw^i)s3XlVlL6-ihx}Q=aHB%O<0b|6Q=5>hQ)5ExfxHGWGK3)@@8kOD2jCtsm-$} zAJZ*BGsdJRD%@J2v}2N&Do^6a^)`4P;(dL%=*AM;k9eVn!+|0uwr%M0_PHwy7amu) zxG>`O2VpM>wmudEwh10U#eqgKEQS)*AP&hW=}v+gEVa3V^ZFQqHbgWORm2^J(nd5O zmCtxf2$UMY_l$QqrH5HxMt}h6=ri7tID+UX+Kx81yl$1G>dGm2HZ+FFSPy}(Re!ZcO-OMMF}!Ky9jZTBqYiL#Qq9)eVQif48N=Zj zbl98WGl6l2&mwiUK6kBJqz)%3Ps}B%IU-cTsfixeO>Eb!TzvX09(${eq_vINcM4zia5Px1?Arn7x}igY+|z z61=k@wtFrq&Jy$Jt?b#th`*IR2TgG3Tne3s(ncxLajT1Uq@3?j;94793$SAhXP_?HV9?|+DQ7Psk}WD+dTuiEEbTnDV4W}mxG8P zHq-bDlr{*FiwojPQnq*$1aTGJ8OY8M#MQufdANquYYmlm7V_*;X3%xCC=xB1LD!?U zcZfI8=tg6N#Dl$6CUB9Sag|DS%rc@>q{Z?ksm)2p=b|^`7;$Vvg)MKCHjW!od8>FU zf!k7ft9W~=xmCPF&pTh0s5|vKycy8xE>iB+H=U37eeR)z|4wmxs_q$ZdKBFY1hhuPI_8Uvz2Bn z+-J~&uAfB}ai2qJM@{-*_G{(dZ71>P^=}ubBZBdL@ZRkOua6vAhUd182dW{0F8v$MkL=s+MAnl|1H^_NY=dD+{ z>VH(5ZPnfahH&v7Qo99p^(`US^y|KjcF4a&(|1wYphYZR{ylPb>&@#`rh1?9RaX83 zVA0WBZl2A5h!zNcM5B*UT49li7yg8tPYs8gn=8CQ`HcO9Z9kub6z#`w+%M3Ae!ir} zS4M;5BdYQJd`-?bhQlN*t#s=utLj6sBzzm6gzxk&jU;>z8+7sm1%5OF>_jBvJNb#6 zpLO0vxJ&8JpU? z1^!#Q;yW(c|BxwnQ0>xWE-RGK)VnWMhbA~Uz{&9^PsQYY^i@!ZYz^?lw)`;6P zt?i%5nNF`X-o=a%Yhm`5+I*HqPfWKDn&53;n)O3zy)Dq|HmdBdg#?NW;9E2Grw>D| zaxn-{j;{9y;0U4vX*&Pb6%{eHIZ-~zT2;rHupM}z9U94kE`Psw|_i*HBPFmaQrG&6j)?b`N8%9R_ruoz0#kKzc7mcC?;9xb(b`6|}QF=&Lp$D)e3b5UA< zk4%%h(wun&Dh$A64HW75lpbJxsRSO))j+6K(Wu%OaaiZ2@pd$`hDgXmAhiHjfZE>r zyT78$(nW|makGHE~y{A(n& zDUNEMUo0lF#6#ep%U--|ShHYuf!acPD~1XyZn53-8n%?zuvyClj9+XoM-!Y`L5<^3 z+MyTKxXal}QjYg1@G3&D0_+pHJpmXN+uN_iVtZwpS!|y~tCRg!r^Lr{s$e#j(?~x( zjf-uu-7}Uoj76>0v$m+LiD!TV|Ieh|Stzalq7>)<*`%D~QQ-f%^kg9Wf&b?LLRIqYwU}0j2JFK z6>&G9v@u+m#x?dv0+*(7jeS{~Sz~X~1Fu%4&gG!sam;2?u1GWQ;$BG&{~~)!nptFD z1q6$1sr-xVt7#yM>}&9syiW7Y)ha~Q+M@FEww^=g=xx`igskhtKDyq%o<>-2-=KG1 zgZ1`}I7S#-sc;j@`1SV9y5qHK(AaIXlJ)j2B;Ly7etc^kuVG=6t`=(waRaH3cE<(x zZT15{aq{hgnt2>GaqmDAI=>TD#Jvlp9Yg7b^MEVvyNTbUpSu<-?)Xl);@<9cl725m zWW{|SjqXn~@4!C5DbOHFlFSFC{?}V;JJ1Ga9-`gDD6KQ11ZR8;?MKLY)NsnNAKb1n zw5-Bj8YdJM$~=0EU6$D{9|s#V z|0-=?LuvIzEnfe1a^BFZZ-D-r@!tMVKX8M}$$bkHsJ%_*J1DK%>uF~3`!139e2APj zv1!J|?{04fq`gn3EPg+r<%fFijcQiPM`BbM7PYvOBl%-#iLQV@5gD@r{!|~hRm~js z8BnG8L^!6nwU+E{es&8_?#(~#c*x$uBri+5i#E#zzsa)^e zsy@sA4u7Mc?U!X4O6LBU;hVPo(^;d)}gM$T#6cJ1A@4*n}IqUqF!g z_p3K1slQ2+IQct`|DbVDlrWBd@BXBB9TVjo`Ah$Giz-X`8!UMC4=vlJ>&jcz5m};x zCCTwvTS8T(%jh`idTR>qdG^uk?o-(#gd#HS0Y*>z!s@EZ=1`&A0ZowXNV85TZO)b6 zaJ%YSnqcrm;zY`WOrn;%*_^t!MY}c;oIt5MWI+a#wC~eoB+4b;r zgJ%#A8vOt3I+LWVbUh@1zO&i)NE>zzK=duI{1;X?wKldi7rI^1hR(auE*GWkJd>SI z%QJXB@$Lry|2pqMQbCj?-OQg~Slzd9{(Q-OPdbxro#_P{oRODPg`?-+p)$j~!iWsd z+3+dIC6_$BUr2KM0fDoHsPNNTj27T*qX2Vc}4yifwD~Ay&(p^md>rxd2LJzhoUVXR9 zoH!dqgnBrQ=b*GBFLEZ7=>2#ZDMxq|xN{`kDPm`EryQ7l7JU@iNBh~wklkN&#Q9?d zv(KXEl0MH*uOPiZgoHj{YPoV&l3Hb`9`=aJ&lM?Q)=C|}2+@)4xFHOs^Fd`ja12rdH5yUcK@x%34Pg`U#U73kTB0{U zVV^*|0Q~oYThe$hXq{*VY;TlWsMKnd!eS`blBJBVM7j~Lmq{Ia0=*oEaB>Bzhtstho?#C|b*-Tjv zF&5%EQkz{=|B6rNJ%F^GY0`E+I2e&O%3Ofbj);iF(sm*7^&Sq_wJ&WK0oY60tRoi# zwrRVBN*jz)SPUg?csm~rKpmKB!XCZvgNgG2aN- z#=MnEHyNd{7)s1H)4MU!jl{f7YO~#Hze`+L=-z@>czG+ThZhA2dmr2G_iW%cZcdOYZ^}Lh zj8k?8sSoK@598U%!z7IqbBXN{5wKJCQ4$}MCVWWC_Oi^*Wudx-wJkC(k8>91i=Nm% zA+;Ho7*AjJ2Mky?Y;`b|zxkS1Vzh9L)HiuusA)I_2Rm6P*rFHTplgKv-{Lcp%@BN8<3n(<+ zCiNXZRi{3trU%|N_-xvk47I}~@_n{_;Mu_6591U05iln5V^TlS`H!o0>Qj=2 zi@5~#nF!cKeoo>S(u73HLdsrdWH`QL8b^tmSbin7`TVdf_iMBvo^Mb^+;36ZG5Opi z_B(@rPy7cTE~2r-{z&{M4@c*CE$Rt1CjA$*?Iowyr(Xfvoc>0U-;GFE3?+y^7=DuU zAVK_@&TlsVg+uuLH>!yH4@&FzZ_H)yb{PUFlol`!`*Nx0KV=`1f2_ZIQY{X203D(o zY1>IRJ%u-rJ~qiqU|XW62gj21Q) z4|xXP%t+7RHvcHG7G{Rjv1B!bS}NTj+TdR%?XpnX*itjNBb;sU9O7MlxQND*)s1+r zha;A}4DO7kKdW-n^8xH7W7e_mfNjQlP^rKug~d=Z){`+ON;eWoFTM3y)ibv@4&i(s zR1vo?O6z=&3|>b25h(Nl#%o{3+#;aR=uc{~pQ;aePMsedVDN#&2N_(?dQP1oFOGM~ zkT-SA>kfm-7-AUs$br{GO{NZr&a+{H*$vAQ(o3~^US(#B?VgKP+(YcLuRDwoM*I%! zNHoFoQ4|`D(nc@RaSPZnq>S|_aBUo23$SAh=y+h*fz5b94OV0H*Uzh>;3OJN_8U#1 zk+%rnPcR$L{-jUM;0CPN?itSkv3Q&VGx)Z{K_DWAgK2ySN*ja7#lQi6D{_Z?bunAj1%KQwo30p}G z|0Zk|9wo|~4%I-g2`iO<6Sjs1vI!f)U-CNCp)aXos$!q%SZ>0;`>N_PeWBP#H(_gO zgiY8wX-wYBPa=c?Ah1QK@J$Sq@!PTudf>}ySXv{1BEG)fMB-xI__FF66i+Y%qCIjS zb_vrjFFs6l1vRftx}k=qr6F!yy3J@tk1Z5xMQMjpq&XY77rT`BGX2lXcrPTrBkslG zM~86UNJptFK;=&c97mg#Vw2e(bLV)@gi#_TNsUPT@25*Apb_q@qUnh!tvko*9k1ZE z@YMuP@&S^_Sk&AWJlS(5?G!TQPVQ7%o~8%As%E5|E=Gl6QHx7g@)~K0ZVRrZjCnWw z4BhoLHGTG(fZ^U*s3Pv!DB)f>DkdG6(GtBexOTa`p8)&@| zr5$b2i@OwDO3Gy(1^#TJKRN6T{#*`>mx9ftUZH<~T^+2hBq>kK#px{~U_UsoBJpa@ zj}8#L#(Z|>iGYx=mHJ;3MF05)maA8p56T-sfcsmia}!GIz9_^#C~qdd&BM{vEgACT zT4}eU&Hou$rPW)i&j9PdZGi1(!-#~%P(E$$WOyaggLr(G)UlQ7-8h7Q_n?Zn z+fiEoZqJart=xMF+-CrDm(k=Fi0=JH*E;e5DA4772pH~zIEL^J+COCMIl=DL>;9** zGan}KhzCINQL{AIF<7OhJqF-E!hRgE4f_cyJ!zD}VkquCMfZ}V8wvYqOs66EQpYnm zhFi~~!tDb}8}Acb8%*6v;CUZl5@kb^-xhlTC^UAF`r=-wdfeM8KgE5?@LneG6+Pl@ z)lY5K$G(kE(O22_nr91#UpLdxo;z;{W(2l?##nv`46A~--@&S2w^p}zd&cv1EFR|@vnu!&M8xnNjlV}}V-UHx7=9q-M~{LSexf@A z*%@N^85l1GzmWQ?p~kHWexpT^Xvqxv9kpE*{6V8XjgeUu{KZv)GN)&$sEX0wQu|f~ z|KJ!QvwHZ&)BVoIvs;t6?7scA!t?wiPZ3~3X+0m zRnQp-Rs~Y|R|Q>YAgh98{1tHnI^}(}Qgza2zOUwIrHD~(fUl*)v5+a@SXCvlrp};#xnn!kt1|7NNB6h!)(z=-HPZ`;$|w2YjS@ssR*FwBiGG z!$&H=WDqD&8%*X9lvYh_Ess*fXV}ar6-;9q{=1g}S4xl-n{0S(nrYOW~P9$ev!!f^;hhK4O4z*0+ z@8ro1$(2Xc9@U)!0KM)<#r=&UKRu+6_+F=ybAX=v34T%R!1!@F zNN@NA-`qGD6sR3S<}{QxhN(gGB==AvhxrgW%VKksOSb8vovBlg!S4-`DNDAQw49~K zeX52e%od}<@S!}Hk`I@o=sInVvE_I8%GQ6T2G2eMD117S_T?zyQ#dLhq6iYpgXl8t zD6)<=ERKVC9RC92G3-K?w>^&)qFtuVrR_Y=7Fnp^DCLW)O!WDrN0(`pq*n#GOcPsg z6uf0xwF!pHw3?t;GPg&PKx z*1yw(+%7qXz_~uaI1UMK*ZDl4&{#+6`F^V2{gs**XfyZ)#4pr`eXS~-mZ14Q(|R^) z&j=1*6rZz;fiY*7kh(z!zEMZ2jU=8 z!1s9NPD&56zT5=@q@#~l?#2;B@1gB>W6R6b?LqU3aIJ z1Z)G}L8XU`QdkTn@Q3MLl5``1KN4KO25;~@ibJ^d7%DtzKxt#$VP^Id1fKK(CT;$i z{S;7WJWc8|eySe+gBlQc*5J<(->DmZPy?MyqBHw>Hoo8)!O>lG)VmtJ2#hoPB~oA3 zfBc~86uw`9g~(_zmuOxU0Xwr_Bk^@Jk?TjI4I`}KTJ8g2~pQ=tK&GtbRaoeM` zW-sZ&U)6qr4hHW?ypvx3tJ>drDLPdX*qG=U!M7x>5}a&rs&)p(soI6qWZnHYJgyFq zlqcpAK#B<1shUb6Uflf+Un)({cd4RHZ{8u&-HM)pO@)P&3v@)B~8|q?tpEYdQhmq2!+K^($tetmP#+;doQVdQ?oaY z5kwzUc;JB22GJvv*VKLl3Ju_!nnjcyYW?UBLTqXl;|QVyXgko@a%%R|ws7~VSsJmC6p^Qa$zx)!0|n8%z<&zmjpgSYV(po^s%++ zXC&I;>nKzacQi^n0414RLX06W)(4pM`DgDqpwJjk>I6SkAODwH9++tGeTh%f&fjW< z(n7iRGbEn;n-Z>%#6%H|Jn8iDv>uXtPj7+}SAY@Q8VA5+6={j)$Y4 zve+a(0_}UrrS;%Qz&4lV6gtWXg~d>OKAORnN-yH_F;e>`@v%6DlXFo;+<7RiljUX- zR}h$Q0N*68r0h`ZM->Q=jV=tTaRkvC+J=lRCvkY7o2kDGs=-~&O`}@} z>_5T|1GZr=qFB8V3yYzIjjwBC3MWcW5_Y5Rnc(E)HsKJCEk=cxl2F=c7jeewjD4K+ zfhIlvN!nt}T4}b_XEs&$+sCO3EHn6W;w$u}`#51|YIKqw$HtYO5qvwIzIl^00*sUN z1X5S&y7tc5>O_)8h`9u?S_JGQJ&DAVr78MUz9uBUeO|?H@t$I4X|BkL>8VmjU+ap} z^wtiTrze?tx)wMFJwy0+2mSLl4ek`o?&sb``rSwXzA_}Xdu|%sBj(Zl>FvUZ-=Dr0 zO^EY83f+&=#wpTq`v(t@@}Nh7Ydh##fE{D#9s-8_>4BY{!OjDj=6ger(du!()f4dn zJt>$C=qb{l&g5fjvE4JEXJP?4&t~$m^>ZL1h@CWk9;FRJcLuUE z1o09uUJG6(^%X;n+n|1x7X3v_X3T4-?FRMhG_cW0U%>i6|!$_ z_Axd5JJg@(f`C&{^eF&rP)p_Cp#F?}*`WR$f8nbQx;Ee(LQy;it>9zpK#G$x{Y$Zr z?p1$9BkWawEsbe=d2Ia+0L1kzs)+j?%J|*u@AaY-XL#BV0L)|SA4&X4Z%c9Vg5n8g zK(t5hSpUrQ%XXXkWqqpCb?~p?po8CN_d7~EaH7QIbD#PT;(zJ`Q=Q@e(&aw&U!HE- z-xQI3>VIg|PFH3**-Gi%X-@WZ5t0NtS*W8Ib?l+?NAveV8+>Vx3jgH*rS(OG;EQjQ zx+6KA^!PLH=w5A4vU(X7KSNA^{zvM)Jy(175u)9z?MvH!o-Iz>LXKFzs7khrNRRH-_9wkK zi+iy3!FS3AH&!@b&pS!SjBoZf^6J?67?B?W;5@R9mC-uKhl>O0$p_odu15nzbn3TmA-48VQLBtRC;UXGa z5gkH&nulYY4%M4`IOeC;(hdXQUmJX)zw71n8DJfo4%n^@W>9ISQ3{KpT)Ss68ly?b((av!iHsw9jn3J|f4N6+Fh^ z#}c1wa6LE2IV5o&4safg&oXyk6=cje4D(@wm*8=pdx@+P14(2xsWrN>tAj6uk~Bih zC8`A?U>CLvNvxG7ENtZ$V7)7l3`iYQnZL5+VdnFo17yyNbd?ttTgf0O9CO*lHX>^d#Dz zY;1X9!uPFvIoZKe2%PExaP%}rozL$8Ex?a{!L`$Y|3~m^0Ndc#Qt%8T7#2eb{!IEf zQhJl%&yw0K0t=&EoQ)x1wQ45Qqgnk(v93@>z=$oWA zSE9n^y2gd|_~zl|XoH)ZQQ^O(p|r7YG_hYvV2c5Kv0oJ*`_&-CV!sAQ5WSYR*BM*J zjxS3{=C3Dkg9pIX8zuJ9XU*vbkM}7|ovB8cZyQ`c(Az0bd|3@%8=IyejH+)we|NAaSZ1)^ zfC$)Im+*MyPz?reC08P`&k{2{JwaB;dEASH+&2CS!d!Z~#K!f}jmS(p%8~aFO zYV(SQs;2r1Yt`)bMe|&@pMYtVtu0L!F?xkWiv$hEo34uX*q@AIVI)@9$v?(x!atN8 zz}A5%<^TAVb*XN3-B4poMSUIqiP|7EPcKVvaVr~}7c{vI6>ze-av?k&OtT><#r?%i zE`D9W9ZGsaU5)(4xZC0m!|^n=sw~;>ZnIlL+fuPjwvLz}hNCUwju0fZuCb=h4OO>P zKx$#ArP&=xvrz&j;B%`59}vy%XdD(**EChPw$#-(55~V3YitQM)+8NKS>GCJPIAX& zw@Yf*6Z-)vb3Y>g|J^UaR(%P7)Dz zHNK`@y~v$}<27Z0#i7OGX9xpSjWL)^fq;BgEv~@N3^!ohn%yaEO>Sut+3H0Y$3@NV zej?ZbxVysLU;a+6uWVe{ilKyIO~{=pZBD4sJwX1Ln}}x^A&en@K3|3QGrRoTv=MzT!*Le zc+`?S)X7uF=;EPHpWerUv$kxXP~%c}F6k*XtqqGS7B`1lYv5o*y*p1Y8R}%Osvw#! zVKui_wbZr1B@^3xN_1{+YIUnaa&jzkD@hK>{P9hSD$_wzYs=!+mI_RirW(ZSR-?JB zLu<=|L8ILo`8z2pC9^JMlsuzzQG1P64 zzdKbJkK9Ha)%Gt-!AaK=THYcvdvPUF-h}owWr+(zxG+@8ATLJ4^s)fQ5;spFx3RK* zK(o7q&0U%sa0#eb(uylyv+JU5ZCMw|vv;mF>%0=DU+)$`a(te_E98#=W}ZKF_*MwavsMDbRX93+ zoNmo{{_|^zlV2PGwx+DJTe%DaB%@s8o`5Dy*knfMU2azCeM_D0xhD$Pu56#Wnr3%3 z{*JgOp)9KH7`|99FLip4;ctH?hePuAaUlGf{EOd|@G#sC)~1@DLP`sBOEW$EG;XbH zGt4Ww!LXxWE_M23oJ`GAP-0VTq%5*d)t!es#RaFK3ICH0Wm!i_JNjQk?plx diff --git a/docs/API/_build/html/genindex.html b/docs/API/_build/html/genindex.html index 9730954ce..38b8302d6 100644 --- a/docs/API/_build/html/genindex.html +++ b/docs/API/_build/html/genindex.html @@ -90,9 +90,11 @@

Quick search

@@ -672,6 +681,14 @@

N

+

O

+ + +
+

P

- +
@@ -419,6 +426,8 @@

C

  • categorize() (quantipy.DataSet method)
  • Chain (class in quantipy) +
  • +
  • clear_factors() (quantipy.DataSet method)
  • clone() (quantipy.DataSet method)
  • @@ -504,10 +513,16 @@

    D

    E

    @@ -516,6 +531,8 @@

    E

    F

    @@ -552,12 +575,14 @@

    G

  • get_edit_params() (quantipy.View method)
  • -
  • get_se() (quantipy.Test method) +
  • get_property() (quantipy.DataSet method)
  • -
  • get_sig() (quantipy.Test method) +
  • get_se() (quantipy.Test method)
  • @@ -377,11 +378,11 @@

    A

    @@ -467,7 +468,7 @@

    C

  • crosstab() (quantipy.DataSet method)
  • -
  • cumulative_sum() (quantipy.Stack method) +
  • cumulative_sum() (quantipy.Stack method)
  • cut_item_texts() (quantipy.DataSet method)
  • @@ -492,7 +493,7 @@

    D

    @@ -555,7 +556,7 @@

    F

    @@ -765,13 +766,13 @@

    R

  • recode() (quantipy.DataSet method)
  • -
  • recode_from_net_def() (quantipy.Stack static method) +
  • recode_from_net_def() (quantipy.Stack static method)
  • -
  • reduce() (quantipy.Stack method) +
  • reduce() (quantipy.Stack method)
  • -
  • refresh() (quantipy.Stack method) +
  • refresh() (quantipy.Stack method)
  • -
  • remove_data() (quantipy.Stack method) +
  • remove_data() (quantipy.Stack method)
  • remove_html() (quantipy.DataSet method)
  • @@ -805,7 +806,7 @@

    R

  • restore_item_texts() (quantipy.DataSet method)
  • -
  • restore_meta() (quantipy.Stack method) +
  • restore_meta() (quantipy.Stack method)
  • revert() (quantipy.DataSet method)
  • @@ -828,7 +829,7 @@

    S

  • (quantipy.DataSet method)
  • -
  • (quantipy.Stack method) +
  • (quantipy.Stack method)
  • select_text_keys() (quantipy.DataSet method) @@ -869,7 +870,7 @@

    S

  • split() (quantipy.DataSet method)
  • -
  • Stack (class in quantipy) +
  • Stack (class in quantipy)
  • start_meta() (quantipy.DataSet static method)
  • @@ -945,7 +946,7 @@

    V

    diff --git a/docs/API/_build/html/objects.inv b/docs/API/_build/html/objects.inv index 702ae2af9f9601f15da7dc415382cac7920df190..4b6e8180792853d538e1be1f3a33fe3128febec9 100644 GIT binary patch delta 1017 zcmV@HNwb8Q5~0jb+ioGQn(H z*W6^gPy5U>&1I{Si^mnT46Of&dfq$WkIa?_sAnX@DZZ4YCm{jAFcsOuqLWX83^aPN z%#$R91Q4Cxkrk2GBIEP09XmZO;pel+1WW-TGzHp&5~_li0{#2P_%6`hV=-I}?TVlM z$IaNR(xq>7){`{`Eq_aicr7;m9Do1B_prIV*>AtaP|qCw%6Zo3xi*pZuTMW0cpVI= z66|K;CCqu&_>N`~K5i!87+jD)d!?}GzicLp%kzx$>T}V5*(}sW=SgqNr^Usk&0NYp zk9?KZ7x;ZM5wFez#&EU>A2)BUdtUhD918zwGxuOD1I`|vVt@R;Sq$gqiBoZ3FaO@k+md)GtY}3g9-d~=kKC=Erv5-2Zz!$B1tMwOfnRqCE!E1iK;Ki5b zKBz=*fr~yqR@1uQv2hD4rTMIF|JYVR*+_7Dz2^7B`4zE3^?BpL0inko0eW|)d0OEI zj9<2K8A_=en|}_D7{h13b?#LR-XJ=A`&wBmWMx;H%A2;mt%U0{;K$*8?<96;*&B(j zw?g^(!my&1?ls>p891Qme|@mD?gr*%Mpsyj4H6n3&tF4Xt@B*!!T}fK^@QSgOz8Cr zee2#V9{!Ueb5?VOi)4-6xI)Fb*yT-)*AuGWGn{%10DnX1w_5YL)L{GZ`}u>OQ2imK zzFuOr-jhgF`cp6Oqki8sq3|Cp97)+}Q(vX^QZB0g?VPmsj{k|?crVH;%yy87N`LX6 z3;*iAk0iZULl19KTED}b*g9RaP%yme&fkdkP9j{4;p6bjt4_rAah-p7+qPdg6qlV9 zw)mzPYJb{jZ!?M>L*?Jb7s6~>*=75|`AOLu@tHz9qw!sTaKUwHZDZTsd=3yCPa88o zE{n;4Vi12Hwgl2@V!M9n6MH3gqV#KP;V;im(g%jO+h3<=gX&A}>Z*Wq*`TXrOG?+VX!Tfz% z!8_(NBVVOTX*jj)ABy;;Dopw=-m`;#_HQp)B`_48S}#))QR%<+^PSUo!^KH$uLK|Z z_;dJ^kCMw+luvYFC8oC%u?y*-E1vypn;71eeUMw62^8@N*Q#Eh{xRUnZVqyNzv8z! n<05?{d^$}PqLiz3Uy5Hw0zZx?wZgw|II$HH{m|x5j>g=x%V88s delta 991 zcmV<510ekU68{pAtON3{C9$s@0|7IW%mYOMS(6e3CV$H_5u2jd1GN_l78v&>mrLc{ zYe2{mMD%^IV%q``4$R3%m{n zR0(!-;U&y@*7%NQ5k77%zA?BUKY69F=)Y_(7MJH4)9Q24f7vY5MdwLx%csTJrOjN* zK978r)))AF^VUA+&=82agw_<;|cpoorF2v;X#4%(q zLa^UUOR;6x{vjOuMzdw}wmsW4Qor|?FH;{`|Dsq(9aG@5*1gsG3%E=?6u;m#zh3a- zOLHGoqPM_%!^i5f?ssh5!b)kLwCx|;DkvKXPOsPeemK7(R;Zpf9vl#Q+!3I66U~RB@p1YZ%4(fcsWS&$jMo#2-!Y-r zEA*{)4EEEi{y7M=py^{zRWB54y@~RVYbzSEl-nQ)*4#j0> zg)P1*hMF`s+S`nx$58pV@r5v(R(9TgaDGzuMm$kyXEeU+4=%Vat!-@Eo6iA)<7s2& z$7OLAj=UC=kq97vtoazMElg6{241ZQx@^7knz9T?;1(EW%aB+(XPfq+ZTz_iKVPP! zb@0nWKGQIg|KH%Gdh_|)XJ{~Y+=BW0Wd-k;&y0MPDy8AnvVSPzm#Q%7yLitI`q{s| zWR<```qFxtl88$Gt)K6lz8fxH)b>j7p^ra@Klv!Rj79lQL>E?KdOH!jkPf=y*}t}l z;a%AWxy3}Fh)1|q_4@RW0atc&kn8&uzs(u%8{yMwst~1It@~X3JQDbEJgF7_eZz^Z Nkm!dte*z8W-*f-O0ObGx diff --git a/docs/API/_build/html/search.html b/docs/API/_build/html/search.html index 61abb326e..e442bf83f 100644 --- a/docs/API/_build/html/search.html +++ b/docs/API/_build/html/search.html @@ -295,6 +295,7 @@
  • quantify.engine
  • QuantipyViews
  • Rim
  • +
  • Stack
  • View
  • ViewMapper
  • diff --git a/docs/API/_build/html/searchindex.js b/docs/API/_build/html/searchindex.js index 2fd158d03..ee2a67ab7 100644 --- a/docs/API/_build/html/searchindex.js +++ b/docs/API/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/api_ref/stack","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/lib_doc/overview","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:52,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\api_ref\\stack.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\lib_doc\\overview.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],drop_duplicates:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],min_value_count:[4,2,1,""],names:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],resolve_name:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],unbind:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[9,2,1,""],count:[9,2,1,""],exclude:[9,2,1,""],filter:[9,2,1,""],group:[9,2,1,""],limit:[9,2,1,""],normalize:[9,2,1,""],rebase:[9,2,1,""],rescale:[9,2,1,""],summarize:[9,2,1,""],swap:[9,2,1,""],unweight:[9,2,1,""],weight:[9,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[10,2,1,""],add_link:[10,2,1,""],add_nets:[10,2,1,""],add_stats:[10,2,1,""],add_tests:[10,2,1,""],aggregate:[10,2,1,""],apply_meta_edits:[10,2,1,""],cumulative_sum:[10,2,1,""],describe:[10,2,1,""],freeze_master_meta:[10,2,1,""],from_sav:[10,3,1,""],load:[10,3,1,""],recode_from_net_def:[10,3,1,""],reduce:[10,2,1,""],refresh:[10,2,1,""],remove_data:[10,2,1,""],restore_meta:[10,2,1,""],save:[10,2,1,""],variable_types:[10,2,1,""]},"quantipy.Test":{get_se:[9,2,1,""],get_sig:[9,2,1,""],get_statistic:[9,2,1,""],run:[9,2,1,""],set_params:[9,2,1,""]},"quantipy.View":{get_edit_params:[7,2,1,""],get_std_params:[7,2,1,""],has_other_source:[7,2,1,""],is_base:[7,2,1,""],is_counts:[7,2,1,""],is_cumulative:[7,2,1,""],is_meanstest:[7,2,1,""],is_net:[7,2,1,""],is_pct:[7,2,1,""],is_propstest:[7,2,1,""],is_stat:[7,2,1,""],is_sum:[7,2,1,""],is_weighted:[7,2,1,""],meta:[7,2,1,""],missing:[7,2,1,""],nests:[7,2,1,""],notation:[7,2,1,""],rescaling:[7,2,1,""],spec_condition:[7,2,1,""],weights:[7,2,1,""]},"quantipy.ViewMapper":{add_method:[8,2,1,""],make_template:[8,2,1,""],subset:[8,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[9,1,1,""],Rim:[6,1,1,""],Stack:[10,1,1,""],Test:[9,1,1,""],View:[7,1,1,""],ViewMapper:[8,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":39,"boolean":[4,6,10,18,35],"case":[0,4,5,6,9,10,12,13,15,18,20,23,24,26,28,34,35,37,38],"class":[2,3,4,5,6,7,8,9,10,11,22,34,35],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,34,35,38,39],"export":[0,3,4,20,35],"final":26,"float":[4,5,9,10,19,20,21,22,23,24,31,34,35],"function":[4,10,20,25,26,28,31,34,35],"import":[4,9,11,15,20,22,25,26],"int":[4,5,9,10,19,20,21,22,23,24,25,34,35],"long":[25,34,35],"m\u00fcller":0,"new":[3,4,5,8,9,10,12,21,23,24,26,31,34,35,38,39],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,34,35],"short":[8,19,21,24,35],"sigur\u00f0sson":0,"static":[2,3,4,10],"switch":[24,34,38],"true":[4,5,6,8,9,10,13,20,21,22,24,25,26,31,34,35,38,39],"try":[4,10,23,35],"var":[4,9,15,34,35,38],"while":[4,15,19,23,24,34,35],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31,34],NPS:31,Not:[19,24,31,34],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,34,35,36,37,38],Their:5,Then:[21,35],There:[13,19,21,22,31,35],These:[15,28,31,35],Use:[4,9,24,35],Uses:4,Using:[10,18,21,23],Will:[4,5,9,10,35],With:[13,22,23,24,26,31],Yes:[20,35],__init__:[12,35],__setitem__:35,_band:4,_batch:[10,31],_cumsum:[31,35],_data:[4,11,21],_dimensions_suffix:35,_get_chain:35,_grid:[35,38],_intersect:[28,31],_meta:[4,11,12,13,22,28,39],_missingfi:9,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:35,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,35],about:[13,19,22,36],abov:[24,31,39],absorb:35,accept:[23,26,35],access:[4,10,19,20,22,35,36],accessor:22,accid:[21,35],accommod:14,accompani:[19,35],accord:[4,9,10,26,31],accordingli:26,account:[4,5,34,35],achiev:21,across:[9,22,35],act:[5,24,28],activ:[19,20,24,35],add:[3,4,5,8,10,13,14,23,24,28,31,34,35,37],add_batch:[12,31],add_chain:3,add_data:10,add_filt:[14,35],add_group:6,add_i:13,add_link:[10,28,31],add_meta:[4,23,34,35,37,39],add_method:8,add_net:[10,31,35],add_open_end:[13,34,35],add_stat:[10,31,35],add_test:[10,31,35],add_tot:35,add_x:[13,31],add_y_on_i:[13,28,31,35],adddit:20,added:[4,10,12,13,14,23,24,28,31,34,35,38],adding:[4,14,18,20,31,35,39],addit:[4,13,19,22,31,34,35],addition:[19,34,35],adjust:[34,35],adopt:13,aerob:24,affix:10,after:[4,9,10,21,26,28,31,34,35],afternoon:22,again:[22,23,31],against:[4,5,9,13,35],age:[4,13,14,22,24,26,28,31,35],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,35],agg:[7,13,31],aggnam:7,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,34,35],aim:[0,35],alasdair:0,alert:34,alexand:0,algorithm:[4,5,9,35],alia:[4,21],align:35,all:[2,3,4,5,9,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,34,35,37,38],allign:35,allow:[4,14,15,23,24,31,34,35],alon:[22,35],along:[2,9],alongsid:[10,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,35],also:[4,9,10,11,12,13,14,22,23,24,26,28,31,34,35,37,38],altern:[4,23,31,37],although:22,alwai:[4,15,23,24,26,31,35,39],amount:[4,14,21,28,31],analysi:[0,35],ani:[4,5,7,9,10,13,19,21,22,23,25,26,34,35],anim:[4,19],anoth:[10,13,23,24,26,35],answer:[4,9,10,19,22,23,24,35],anymor:[34,35],anyth:26,anywher:10,api:4,appear:[4,35,37],append:[4,5,18,31,38],appli:[4,5,8,9,10,15,20,21,22,24,28,31,35,39],applic:[4,24,26],apply_edit:4,apply_meta_edit:10,apporach:21,approach:37,appropri:[4,5],arab:35,arbitrari:10,arbitrarili:[4,21,25,26],archiv:33,argument:[5,8,11,20,23,24,35],aris:28,arithmet:9,around:[4,35],arrai:[4,9,10,11,18,21,22,23,31,34,35,36,37],array_item:35,array_var:39,array_var_1:39,array_var_2:39,array_var_3:39,array_var_grid:39,arriv:9,as_addit:[13,34,35],as_delimited_set:35,as_df:9,as_float:35,as_int:35,as_singl:35,as_str:35,as_typ:[4,35],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,9],aspect:35,assess:[0,38],assign:[4,6,35,37],assignd:24,associ:[2,10],assum:[4,9,28,38],attach:[4,10,21,23,35],attempt:[4,34],attribut:[2,4,11,22,28,31,34,35],audit:35,auto:[4,10,34],autom:[0,10],automat:[4,13,14,20,22,23,24,31,34,35,39],auxiliari:9,avail:4,avoid:26,axes:4,axi:[2,3,4,5,7,9,10,15,19,22,35],axis_edit:[4,35],b_d:4,b_filter:34,b_name:31,back:[4,5,38],badli:[4,35],band:[4,10,18,34,35],band_numer:35,bank:3,bank_chain:3,bar:35,base:[4,5,7,8,9,10,18,20,22,31,35],base_al:9,base_text:15,based_on:[4,35],baselin:8,basi:26,basic:[20,22,25,27,35],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,10,15,28,31,34,35],batch_nam:[4,10],batchnam:[11,13],bchain:3,becaus:[24,26,35],becom:[4,9,26,35],been:[4,9,10,13,21,24,26,34,35,39],beer:38,befor:[4,9,10,13,14,24,26,31,35],begin:[4,26],behaviour:[5,7,22,26,35,39],being:[4,24,39],belong:[4,11,12,13,31,34,35,38],below:[4,9,13,26,35],benefici:21,benefit:26,better:28,between:[4,9,13,31,35],bia:9,big:21,binari:35,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,blacklist:[4,35],blank:37,blueprint:35,board:[19,24,39],bool:[4,5,8,9,10,22,38],border:4,both:[4,10,12,13,19,20,21,23,24,26,28,34,35,39],bottom3:10,bottom:35,bound:35,bracket:[10,35],brand:38,break_bi:[13,34],breakfast:22,brief:35,broader:19,buchhamm:0,bug:35,bugfix:[34,35],build:[0,3,4,5,9,15,18,19,35],built:[14,31],bunch:34,by_nam:[4,35],by_typ:[4,21,22,35],bytestr:2,cach:[4,10],calc:[5,7,9,10,31,35],calc_onli:[5,31],calcul:[4,5,9,10,14,27,34,35],call:[4,10,21,22,24,28,34,35,39],came:35,can:[2,4,5,9,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,34,35,37,38,39],cannot:[26,34,35],cap:6,carefulli:4,carri:5,case1:38,case2:38,case3:38,casedata:[4,20],cast:35,cat:[4,19,23,34,37,39],cat_nam:[10,35],categor:[4,5,10,18,22,23,27,34,35,36],categori:[4,5,10,19,22,23,34,35,37],categorized_nam:[4,35],caught:35,caus:[34,35],caution:4,cave:[19,24,39],cbase:[10,31,35],cbase_gross:35,cell:[5,9,11,26,35],cell_item:14,cellitem:35,central:5,certain:[4,22,35],chain:[0,1,3,16,22,35],chainmanag:34,chainnam:2,chang:[4,6,9,10,18,21,24,34,35],charact:[4,23,25],characterist:[19,24],chart:[0,35],check:[4,5,9,10,22,26,34,35],check_dup:35,checking_clust:10,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,10,21,24,35],clean_text:[23,35],clean_up:[4,35],clear:35,clear_factor:4,client:34,clone:[4,18],close:12,cluster:[0,1,2,10,13,14,15,35],code:[4,5,6,7,9,10,18,19,22,23,24,31,34,35,37,39],code_count:[4,22,25,35],code_from_label:[4,35],code_map:4,codes_from_nam:[4,35],codes_in_data:[4,22,35],cola:38,collect:[4,5,10,11,13,14,19,21,22,24,27,31,35],collect_cod:[10,35],colour:35,coltest:[5,8,10],column:[2,4,5,6,7,9,10,18,20,22,23,24,26,31,34,35,38,39],combin:[4,5,10,16,22,25,26,28,31,35],combind:38,come:35,comma:25,common:[7,19,24,35],commun:10,compabl:4,compar:[4,5,9,34,35],comparison:9,compat:[4,10,20,35,36],compatibilti:39,complet:[4,9,35],complex:[4,5,6,18,19,21,22,26,31,34,35],compli:[4,26],complic:18,compon:[4,5,7,10,11,15,18,21,22,23,24,35],compos:10,compound:3,comprehens:26,compress:10,comput:[0,4,5,8,9,10,27,28,31,35],concat:[2,22,35],concaten:[2,26],concern:31,cond_map:4,condit:[4,7,9,18,21,22,25,34,35],confirm:10,conflict:35,conjunct:35,connect:[4,10,12,13,35,39],consequ:[28,37],consid:[4,5,9,10,24,34,35],consist:[4,9,11,19,21,23,24,35],constitut:13,constrcut:10,construct:[4,11,13,14,19,21,23,28,31,35,39],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,35,38],content:[21,28],context:[19,23],contrast:[19,24],contributor:0,control:[4,5,7,9,10,23,24,26,35],convcrit:6,convent:[4,9,35,39],convers:[4,18,34,35],convert:[0,4,9,20,24,34,35,39],coordin:8,cope:28,copi:[2,4,8,12,18,21,23,26,34,35,39],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,35],copy_not:[4,35],copy_of_batch1:12,copy_onli:[4,35],copy_to:[4,23,35],core:[5,20,22,25,34,35,37],correct:[9,23,35],correctli:[34,35],correspond:[4,5,9,22,23,24,35,37,39],correspons:35,corrupt:34,could:[23,24,26,35],count:[4,5,7,9,10,31,34,35,38],count_not:[4,22,35],count_onli:[4,22,35],counterpart:35,counts_cumsum:[31,35],counts_sum:31,cpickl:2,crash:35,creat:[2,3,4,6,9,10,11,13,14,15,16,18,20,22,24,28,31,34,35,36],create_set:[4,35],creation:[9,18,23,35],cross:[13,28,31,35],crossbreak:28,crosstab:[4,24],crunch:35,csv:[0,4,10,11,18],cum_sum:9,cumul:[7,9,10,27,35],cumulative_sum:[10,31],current:[0,2,4,10,24,35],custom:[4,10,16,18,20,24,34,35],custom_text:[10,31],customis:35,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:9,cycl:[4,8],dai:[22,26],danish:19,data:[2,4,5,6,7,9,10,11,14,15,20,21,23,24,28,31,34,35,36,38],data_df:[4,35],data_kei:[4,10,14,28,35],datafil:[4,35],datafram:[3,4,6,7,9,10,18,19,21,22,23,28,31,34,35],datakei:35,dataset:[0,1,10,11,12,13,18,20,22,24,28,31,34,35,36,37],dataset_left:4,dataset_right:4,datasmoothi:0,datast:34,date:[4,10,19,20,21,22,23,24,35],dates_as_str:20,ddf:[0,4,35],deafult:[5,9],deal:20,decim:4,deciph:[0,18],deck:0,decod:[10,34],decode_str:10,decor:35,deep:4,deepcopi:35,defin:[2,3,4,9,10,13,14,19,21,23,28,31,34,35,37],definit:[2,4,5,9,10,14,19,24,26,27,28,34,35],definiton:[8,19,35],defint:[0,2,4,5,6,9,10,14,19,35],defintit:[21,39],defintiton:23,del:35,deleg:26,delet:[4,10,35],delimied_set:22,delimit:[4,10,19,20,21,22,23,24,25,26,34,35,37],delimited_set:[22,24,35],demograph:21,depend:[5,13,14,34,35],deprec:[10,35],deriv:[4,9,18,21,34,35,37],derivc:8,derive_categor:35,derot:[4,35,36],desc:4,descend:4,describ:[0,2,3,4,10,14,19,21,28,31,35],descript:[4,5,8,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,35,37],detail:[7,18,19],detect:[4,6,34],determin:[4,5,10],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,35],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,35,38],dictat:21,dictionari:[4,6,7,8,10,35],differ:[4,5,7,9,10,13,19,20,23,24,28,31,34,35,36,38],digest:19,dim:[5,9,31],dim_comp:35,dimens:[0,4,5,9,18,35,36],dimension:[4,35],dimensionizing_mapp:4,dimensions_comp:[4,20,35,39],dimlabel:35,dinner:22,direct:26,directli:[2,4,22,35],discret:[10,13],disk:[4,21,35],dispers:5,distinct:[19,31],distribut:[5,9],div:[10,35],dive:[19,24,39],divid:9,dk_filter:4,dms:4,dmsrun:4,doc:[22,35],docstr:6,document:[0,4,10,19,20,23,35],doe:[4,11,23,26,35],doesn:[4,25,26],dog:19,don:[19,22,24,37],done:[4,13,14],doubl:22,down:39,downbreak:[28,31,35],download:39,draft:35,draw:[21,24],drawn:[4,35],drink:38,drink_1:38,drink_2:38,drink_level:38,driven:0,drop:[4,5,9,10,21,35],drop_cod:[10,35],drop_delimit:4,drop_dupl:4,drope:[4,38],dropna:[4,6,35,38],dropx:4,dto:39,dtype:[20,22,26,35],due:34,dump:20,dupe:34,duplic:[4,23,34,35],durat:22,dure:[4,9,10,24],each:[2,4,5,6,10,13,14,19,23,24,28,31,35,38,39],eagleston:0,eas:35,easi:[0,19,22,23,35],easier:[12,22,28,34],easiest:22,easili:[4,12,20,31,35],ebas:[10,35],echo:4,ect:35,edit:[0,4,7,9,13,14,15,18,19,21,24,26,35],edit_param:7,eff:9,effect:[6,9,26,35],effici:35,ein:[19,23],either:[4,5,9,13,19,21,22,23,26,35],element:[4,9,10,19,35,39],eleph:4,els:26,emploi:19,empti:[4,10,22,24,26,34,35],empty_item:[4,35],en_u:10,enabl:[23,35],encod:[4,34,35],encount:20,end:[2,4,10,11,12,34,35],end_tim:22,enforc:35,eng:35,engin:[1,27,34],english:[19,35],enhanc:34,enough:4,enrich:35,ensur:[4,11,35],enter:[4,14],entir:[4,9,20,35],entri:[4,6,9,10,19,26,35],enumer:[4,10,23,24,26,37],environ:4,eponym:35,eqaul:4,equal:[4,9,35],equip:31,equival:[4,39],eras:[4,10],error:[4,9,34,35],escap:4,especi:[22,26,31],estim:9,etc:[4,19,21,28,31,35],ethnic:[13,22,28,31],even:[4,31,35],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,35],exactli:[4,24,25],exampl:[2,4,5,10,11,18,19,20,22,24,25,28,31,35,38,39],excel:[0,3,4,5,35],except:[4,21,35],exchang:[9,23],exclud:[4,5,7,9,10,31,35],exclus:[4,25,35],execut:4,exercis:[22,24],exist:[4,9,10,12,13,18,21,23,24,26,31,34,35],expand:[4,7,9,10,31],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,10,35,37],explor:19,expos:23,express:[4,5,6,9,10,22,25,35],ext_item:[4,35],ext_valu:4,ext_xk:35,ext_yk:35,extend:[4,5,10,13,18,22,31,35],extend_cod:[10,35],extend_filt:14,extend_i:[13,35],extend_item:[4,35],extend_valid_tk:35,extend_valu:[4,23,35,37],extend_x:[34,35],extens:[3,4,10,19,20,35],extra:35,extract:14,extrapol:25,factor:[4,6,10,35],factor_label:[10,35],factormap:4,fail:[4,35],failur:20,fall:[4,5,9],fallback:7,fals:[4,5,9,10,13,20,22,23,24,26,31,34,35],fast_stack_filt:35,favour:9,featur:[19,35],feed:[4,35],feedback:4,femal:[4,22],few:[11,21,22,35,38],figur:9,file:[2,3,4,5,10,11,20,21,22,28,35,39],file_nam:20,filenam:[2,10],fill:[4,18,34,35],fillna:[4,18],filter1:35,filter2:35,filter:[4,6,9,10,11,15,18,28,31,34,35],filter_1:35,filter_2:35,filter_def:6,filter_kei:[10,14],filter_nam:35,find:[4,11,22,28,34,35],find_duplicate_text:4,finish:[4,21,35],finnish:19,first:[4,5,9,13,19,21,26,31,35],fit:[19,23,24],fix:[4,15,35],flag:[4,5,35],flag_bas:9,flat:4,flatten:[4,24,35],flexibl:[6,10,24],float64:20,folder:20,follow:[4,7,9,11,14,15,19,20,21,22,23,24,26,28,31,35,38,39],folow:26,footbal:24,forc:[26,35],force_text:[4,23,35],forcefulli:4,form:[3,4,9,10,14,20,23,24,25,26,28,35],format:[0,4,6,9,19,20,23,26,31,35,38],former:[4,10,22,23,24,35],fortnight:22,found:[2,3,4,9,10,20,22,24,26,31,34,35],four:[28,31],frang:[4,25,26,35,38],freez:10,freeze_master_meta:10,french:19,frequenc:[4,5,7,8,9,28,31,35],freysson:0,from:[0,2,3,4,5,9,10,12,14,15,18,19,21,24,25,26,28,31,34,35,38],from_batch:[4,35],from_compon:[4,20,35],from_dichotom:[4,35],from_excel:[4,35],from_sav:10,from_set:[4,20,35],from_stack:[4,35],front:35,fulfil:4,full:[3,4,5,10,21,23,25,35,39],fullnam:31,fully_hidden_arrai:4,fun:35,further:21,futur:[10,35],geir:0,gender:[4,13,14,15,22,24,26,28,31,35,38],gener:[2,4,5,7,8,10,11,20,22,23,26,31,35],generate_report:35,german:[19,23],get:[4,7,10,11,12,13,14,22,28,31,35,37],get_batch:[4,12,13,28,31,34],get_edit_param:7,get_properti:4,get_qp_dataset:35,get_s:9,get_sig:9,get_statist:9,get_std_param:7,getter:34,give:[13,26,35],given:[4,5,6,9,10,20,26,34,35],global:[4,7,9,14,15,21,35],goe:4,going:37,grab:34,greater:4,grid:[4,35],griffith:0,group:[4,5,6,7,9,10,19,21,22,24,25,28,31,35],group_nam:6,group_target:6,grouped_ag:24,grp:9,grp_text_map:31,guid:35,gzip:10,hack:4,had:26,hand:24,handl:[0,6,8,9,10,20,31,34,35,37,39],handler:35,happen:[4,24],happend:4,has:[2,4,10,12,15,19,21,25,26,31,34,35,38],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:7,have:[4,9,10,13,20,23,24,25,26,28,34,35,38],head:[20,22,23,24,26,35,39],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,10,26,28,35,39],hidden:[4,34,35],hide:[4,15,34,35],hide_empti:35,hide_empty_item:[4,35],hide_on_i:4,hide_valu:[4,35],high:[5,9],higher:9,hmerg:[4,34,35],hockei:24,hold:[2,4,9,10,34],horizont:[4,18],household:20,how:[3,4,10,14,19,22,24,28,31,37,39],howev:[23,24,26,37,39],hrafn:0,html:[4,23,35],hub:34,ident:[4,20,24,34,35],identif:4,identifi:[4,5,23,35,38],ids:4,ignor:[4,9,10,22,24,35],ignore_arrai:4,ignore_cod:4,ignore_flag:9,ignore_item:[4,24,35],ignore_valu:[4,24],ill:23,implement:[9,10,19,22,35],impli:4,implicitli:9,impract:28,impute_method:6,incl:[10,35],includ:[2,3,4,5,9,10,13,25,26,28,31,34,35,38],inclus:[24,35],incom:[4,9,24],inconsist:[4,15,23,34,35],incorrect:35,incorrectli:35,independ:[4,9],index:[0,2,4,9,10,35],indic:[4,5,9,10,23,24,26,35],individu:[3,4,35],industri:20,infer:[10,20,24,35],info:[4,18,19],inform:[0,4,7,8,13,14,15,19,20,23,26,31,35,37,38],inherit:[11,35],inhomogen:9,init:26,initi:[4,18,35,38],inject:[4,26],innermost:7,inplac:[4,9,10,18,21,26,35],input:[4,6,9,10,11,20,35],insert:[4,13,35],insid:[2,4,8,9,19,20,22,23,35],inspect:[4,18,35],instal:[4,35],instanc:[2,3,4,8,9,10,11,14,20,21,22,23,24,28,31,34,35],instead:[4,9,10,20,25,26,34,35],instruct:[5,9,19,26,31,34,35],int64:[20,22],integ:9,integr:22,intend:10,inter:[4,6],interact:[2,3,4,21,22,35],interfac:0,interim:35,interlock:[4,18,35],intern:[2,34,35],interpret:[4,25],intersect:[4,18,22,35],intro:21,introduc:9,involv:26,iolocal:[10,20],ioutf8:10,ipython:[4,21,35],is_arrai:[31,35],is_bas:7,is_block:31,is_count:7,is_cumul:7,is_dat:35,is_delimited_set:35,is_float:35,is_g:[4,25,35],is_int:35,is_like_numer:[4,22,24,35],is_meanstest:7,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:7,is_pct:7,is_propstest:7,is_singl:35,is_stat:7,is_str:35,is_sum:7,is_weight:[7,31],isol:35,issu:[20,34,35],ist:[19,23],item:[4,9,11,13,19,20,21,22,23,24,31,35,37,38,39],item_nam:[4,35],item_no:[4,35],item_text:[4,22,35],iter:[8,22,35],its:[2,4,5,7,9,10,11,12,13,14,15,19,20,22,24,25,26,31,35,38,39],itself:[4,9],jame:0,jjda:20,jog:24,join:[4,22],json:[4,10,11,18,35],jupyt:[4,21,35],just:35,keep:[4,8,9,21,23,24,35,38],keep_bas:9,keep_cod:[9,35],keep_origin:4,keep_variable_text:[4,35],kei:[2,4,5,7,10,11,14,19,23,24,26,31,35,38],kept:[4,9,11,35],kerstin:0,keyword:[5,8,35],kind:[22,24,28,35],kite:[19,24,39],know:[14,19,22,24,37,39],kritik:31,kwarg:[4,5,7,8,35],lab:10,label:[4,5,10,14,19,20,22,23,24,26,34,35,37,38,39],lack:0,lang:19,languag:[11,18,23,35],larg:[14,21,28,31,35],last:4,lastli:34,later:[4,13,20,35],latest:[0,33,35],latter:[22,35],lead:[24,31,35],least:[4,22,23],leav:[23,24],left:[4,9,34,35],left_id:4,left_on:[4,35],legaci:4,lemonad:38,length:[4,35],less:[22,34,38],let:[23,24,28],level:[4,5,9,10,14,19,26,31,35,38],lib:[4,19,35,39],librari:[0,19,35],lift:24,like:[0,4,5,10,13,14,19,21,22,24,26,28,31,34,35,38,39],limit:[4,9,24,26,35],link:[2,4,5,7,8,9,10,11,14,27,31,35],list:[2,4,5,6,8,9,10,13,18,19,21,22,23,24,26,34,35,37,38],list_vari:35,listen:26,load:[2,3,4,10,11,35],load_cach:10,loc:31,local:[10,13,22,28,31],locat:[2,4,10,35],logic1:35,logic2:35,logic:[4,5,7,9,18,22,24,26,34,35],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:35,look:[4,13,24,26,34,35,38,39],loop:[35,38],lose:[13,24,38],lot:[4,21,22,26],low:[4,5,9,34],lower:[4,9,34,35],lower_q:9,lunch:22,machin:[4,20,35],made:2,mai:[4,25,26],main:[4,6,9,13,14,19,24,31,34,35],main_filt:35,mainli:3,major:9,mak:35,make:[4,5,22,24,28,35],make_summari:[13,35],make_templ:8,male:[22,26],manag:[0,18,26],mani:[13,28],manifest:4,manipul:[9,13,15],manual:[4,5,35,37],map:[4,5,6,8,9,10,18,20,23,35,37],mapper:[4,18,25,35,38],mapper_to_meta:4,margin:[5,9],mark:9,market:[0,20],mask:[4,18,21,22,23,35,37,38,39],mass:5,massiv:26,master:[34,35],master_meta:10,match:[4,6,10,20,24,35,39],matric:9,matrix:9,matrixcach:10,matter:[11,34],max:[9,10,31,35],max_iter:6,mdd:[0,4,20,35],mdm_lang:35,mean:[4,5,6,7,9,10,13,15,20,22,24,25,26,31,35,38],measur:[5,9],median:[6,9,10,31,35],membership:[22,35],memori:[21,35],memoryerror:35,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,34,35],merge_exist:[4,34],merge_text:[4,35],messag:35,meta:[4,5,7,10,11,12,14,15,18,20,21,22,24,26,31,34,35,37,38,39],meta_dict:[4,35],meta_edit:[4,10,15],meta_to_json:[4,35],metadata:[0,4,10,18,19,20,24,26,34,35,37,39],metaobject:35,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,34,35,37,38,39],metric:[5,9,10],mid:[5,9,22,35],middl:35,might:[10,21,23,24,34],mimic:[5,9],mimick:[5,9,22],min:[4,9,10,31,34,35],min_value_count:[4,34],minimum:5,minor:35,mismatch:35,miss:[4,6,7,14,20,22,23,24,34,37,39],missing_map:[4,35],mix:[4,24],mode:[6,10,20,35,36],modifi:[4,9,10,15,24,34,35],modu:35,modul:[0,4,10],month:22,more:[2,4,22,25,26,31,34,35],morn:22,most:[9,21,22,26,34],mous:4,move:[0,4,21,35],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,38],mul:10,multi:[5,9,19],multiindex:9,multipl:[0,3,4,5,19,22,23,25,26,35],multipli:9,multivari:9,must:[4,6,10,20,21,23,26,31,35,38],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,34,35,37,38,39],name_data:[35,39],nan:[4,9,20,22,23,24,26,28,31,35,39],nate:4,nativ:[0,4,18,35],natur:[9,21,22],necessari:[9,21,35,38],need:[4,8,9,10,21,23,24,34,35,39],neg:35,nest:[7,10,19,25,26],net:[5,7,9,10,18,27,34,35],net_1:[10,31],net_2:[10,31],net_3:10,net_map:[10,31],net_view:35,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:35,new_cod:9,new_column:4,new_d:35,new_data:10,new_data_kei:10,new_dataset:4,new_int:23,new_meta:10,new_nam:[4,23,24,34,35],new_ord:[4,21,35],new_rul:35,new_set:[4,35],new_singl:23,new_stack:2,new_text:[4,23],new_var:35,new_weight:10,newli:34,next:[4,10,11,19,31,35,39],no_data:24,no_filt:[4,10,28,31,35],non:[4,10,22,25,27,35],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,34,35,37,39],none_band:35,nonea:4,normal:[4,5,9,10,35],norwegian:19,not_al:18,not_ani:[4,9,18,35],not_count:18,notat:[5,7,9,10],note:[2,4,5,26,31],notebook:[4,21,35],notimplementederror:[15,35],now:[13,14,21,31,34,35,38],num:5,number:[4,6,9,20,22,23,24,34,35],numer:[4,5,10,18,19,23,24,31,34,35,37],numpi:[0,9,35],obei:22,object:[2,3,4,5,9,10,18,20,21,24,26,28,31,35,37],obscur:26,observ:0,obvious:31,occur:26,oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,35,37,39],often:[21,22,24],old:[4,5,10,35],old_cod:9,old_nam:35,older:35,omit:26,omnibu:34,on_var:[10,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,34,35],ones:[4,9,15,21,22,34],onli:[4,8,9,10,11,13,14,21,23,24,26,28,31,34,35,37,38],only_men:24,only_typ:[4,10],onto:26,oom:35,open:[0,11,12,34,35],oper:[4,5,20,22,24,25,26,35],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,9,10,13,14,19,22,23,31,34,35],order:[2,4,11,18,19,24,34,35],ordereddict:[3,4,10,13,28,31],organ:16,orgin:9,orient:[2,35],origi:4,origin:[4,9,10,21,24,34,35],other:[4,5,9,10,11,12,13,19,23,24,25,26,34,35,38],other_sourc:[10,31,35],otherwis:[4,34,35],our:[0,22,26],out:[5,12,14,24,31,35],outcom:4,outdat:35,output:[4,9,10,13,22,35],outsid:35,over:[4,8,9,10,35],overcod:[4,10,31],overlap:9,overview:[4,10,22,28,35],overwrit:[4,15,21,26,34,35],overwrite_margin:9,overwrite_text:4,overwritten:[4,10,35],ovlp_correc:9,own:[13,35],pack:4,packag:5,paint:[3,35],painter:35,pair:[4,5,9,18,23,26,35,37],panda:[0,3,4,6,9,10,19,20,22,26,35],pane:4,parachut:[19,24,39],parallel:10,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,34,35,38],parent:[4,19,20,35],pars:5,part:[4,7,19,21,22,26,35],parti:18,particip:20,particular:4,pass:[4,5,9,10,21,22,24,34,35],past:26,path:[2,3,4,10,20,35],path_clust:3,path_csv:20,path_data:[4,39],path_ddf:[4,20,35],path_json:20,path_mdd:[4,20,35],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:10,path_txt:20,path_xlsx:[4,35],path_xml:20,pct:4,peopl:20,per:[4,5,6,9,10,12,20,22,23,24,26,35,39],percentag:[7,9,10,31,35],perform:[4,5,7,9,10,21,26,31,34,35],perman:21,physic:35,pick:[4,24],pickl:2,pilat:24,pivot:10,place:[9,26,34,35],plai:22,plain:[0,7,20],plan:[11,13,14,19,24,28,31],pleas:[10,21,22,34,35],point:19,pointer:19,pool:9,popul:[4,11,13,14,27,31,35],portion:10,posit:[4,9,21,22,23,28,34,35],possibl:[3,4,5,10,12,13,19,22,23,25,31,34,35,38],power:0,powerpoint:[5,35],powerpointpaint:35,pptx:35,pre:[4,26,31],precis:[26,35],prefer:22,prefix:[4,10],prep:25,prepar:[3,21,23,31,34,35],present:[4,9,35],preset:[4,10],pretti:[4,26],prevent:[4,15,21,23,24,34,35],previou:[21,35],previous:[4,35],primarili:4,print:[13,22,28,31,35],prior:[4,24],prioriti:35,probabl:[19,24,35],problem:[34,35],process:[0,4,8,10,20,21,22],produc:[5,9,13,24],product:[35,38],profession:[4,35],progress:[4,35],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[34,35,39],properli:39,properti:[4,9,11,20,35],proport:[5,6,7],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,35,37],proxi:10,purpos:19,put:10,python:[4,34],q01_1:4,q01_3:4,q11:35,q11_grid:35,q12:38,q12_10:38,q12_11:38,q12_12:38,q12_13:38,q12_1:38,q12_2:38,q12_3:38,q12_4:38,q12_5:38,q12_6:38,q12_7:38,q12_8:38,q12_9:38,q12_:38,q12a:38,q12a_10:38,q12a_11:38,q12a_12:38,q12a_13:38,q12a_1:38,q12a_2:38,q12a_3:38,q12a_4:38,q12a_5:38,q12a_6:38,q12a_7:38,q12a_8:38,q12a_9:38,q12a_grid:38,q12b:38,q12b_10:38,q12b_11:38,q12b_12:38,q12b_13:38,q12b_1:38,q12b_2:38,q12b_3:38,q12b_4:38,q12b_5:38,q12b_6:38,q12b_7:38,q12b_8:38,q12b_9:38,q12b_grid:38,q12c:38,q12c_10:38,q12c_11:38,q12c_12:38,q12c_13:38,q12c_1:38,q12c_2:38,q12c_3:38,q12c_4:38,q12c_5:38,q12c_6:38,q12c_7:38,q12c_8:38,q12c_9:38,q12c_grid:38,q12d:38,q12d_10:38,q12d_11:38,q12d_12:38,q12d_13:38,q12d_1:38,q12d_2:38,q12d_3:38,q12d_4:38,q12d_5:38,q12d_6:38,q12d_7:38,q12d_8:38,q12d_9:38,q12d_grid:38,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,38],q1_2:[4,26,38],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:35,q4a_1:35,q4a_2:35,q4a_3:35,q4a_grid:35,q5_1:[19,21,22,24,35,39],q5_2:[19,21,22,24,35,39],q5_3:[19,21,22,24,35,39],q5_4:[19,21,22,24,35,39],q5_5:[19,21,22,24,35,39],q5_6:[19,21,22,24,35,39],q5_grid:39,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:39,q6_net:31,q6copi:39,q6new:39,q6new_grid:39,q6new_q6copi:39,q6new_q6copy_grid:39,q6new_q6copy_tran:39,q6new_q6copy_trans_grid:39,q7_1:[21,22,35],q7_2:[21,22,35],q7_3:[21,22,35],q7_4:[21,22,35],q7_5:[21,22,35],q7_6:[21,22,35],q7_grid:39,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:38,q_label:[4,35],qtp:38,qtype:[4,23,34,37,39],qualifi:[4,5,9],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,35,37,39],quantipyview:[1,10,35],quantiti:[9,35],queri:[2,4,6,10,18,19,24,35,39],question:[4,9,19,24,26,31,35,38],questionnair:21,quick:[4,22,35],quickli:[6,21,22,24,35],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,34,35],rake:35,rang:[4,5,18,21,35],rate:[35,38],raw:[5,9],raw_sum:9,rbase:10,read:[0,4,20,35],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,35,39],read_spss:[4,20],rebas:9,rebuild:24,rec:[4,35],receiv:38,recod:[0,4,10,18,35],recode_from_net_def:10,recode_seri:4,recoded_filt:35,recommend:24,record_numb:[13,22],reduc:[4,9,10,21,35],reduced_d:21,reduct:4,refactor:35,refer:[4,9,10,19,23,26,28,31,35],referenc:[10,13,19,26,35],reflect:[4,9,21,35],refresh:10,refus:[19,24],regard:[4,35],region:[4,35],regist:[4,22,35],regroup:[4,34],regular:[4,19,31,35],regularli:[22,23,24],reindex:4,reintroduc:35,rel:21,rel_to:7,relat:[2,7,23,26,35],relation_str:7,relationship:10,relev:[23,35],religion:22,reload:[21,35],remain:[4,5,21,26,35],rememb:37,remind:37,remov:[4,5,9,10,12,13,18,34,35],remove_data:10,remove_filt:35,remove_html:[4,35],remove_item:4,remove_valu:[4,34],renam:[4,18,24,34,35,39],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,34,35],reorder_item:[4,35],reorder_valu:4,reorgan:0,repair:[4,34,35],repair_text_edit:[4,35],repeat:[21,28],repetit:26,replac:[4,9,13,23,26,31,35],replace_i:[13,35],replace_text:[4,35],report:[0,4,5,6,34,35],reposit:[4,21,35],repres:[4,26],represent:[7,9,10,19,24,34],request:[5,10,13,21,23,26,31,35],request_view:35,requir:[4,21,23,35,39],rescal:[5,7,9,10,31],research:[0,20],reset:[4,35],reset_index:4,resid:35,resolv:34,resolve_nam:[4,34],resp:34,respect:[4,9,24,34,35,37],respond:[9,21,38],respons:[19,22,25,26,35,38],responsess:35,restor:[4,10,21,35],restore_item_text:4,restore_meta:10,restrict:[4,5,9,10,19,21,22,35],result:[3,4,5,8,9,10,16,20,22,23,24,26,28,35],result_onli:9,retain:8,retriev:9,revers:[24,25],revert:[4,21,35],right:[4,34,35],right_id:4,right_on:[4,35],rim:[1,4,35],roll:4,roll_up:4,rollback:[18,35],rolled_up:4,round:6,row:[4,5,9,18,20,22,35,39],row_id:4,row_id_nam:4,rule:[4,35,39],run:[4,9,10,15,24,28,31,35],safe:[4,23],safeguard:4,sai:26,same:[3,4,10,13,19,20,22,26,28,34,35,38,39],sampl:[5,7,9,34,35],sample_s:14,sandbox:35,satisfi:35,sav:[4,10,20],save:[2,3,4,10,21,28,35],savepoint:18,scalar:35,scale:[5,6,9,19,35,38],scan:4,scenario:39,scheme:[4,6,19,35],scratch:[18,35],script:4,search:4,second:[4,5,9,15,31],sect:4,section:[7,9,11,14,21,26],see:[13,21,24,26,28,31,35],seen:[26,39],segemen:26,segment:18,select:[4,8,10,13,14,21,22,31,35,38],select_text_kei:4,self:[2,4,9,10,26,28,35],sem:[9,10],semi:34,sensit:[4,34],separ:[4,26,37],septemb:33,sequenc:4,seri:[4,19,22,26,35],serial:2,session:[21,35],set:[3,4,5,6,8,9,10,11,12,13,18,19,20,22,23,24,26,28,34,35,37],set_cell_item:14,set_col_text_edit:35,set_column_text:35,set_dim_suffix:35,set_encod:4,set_factor:4,set_item_text:[4,23,35],set_languag:14,set_mask_text:35,set_miss:[4,35],set_opt:35,set_param:9,set_properti:[4,15],set_sigtest:[14,31,35],set_target:6,set_text_kei:4,set_unwgt_count:35,set_val_text_text:35,set_value_text:[4,15,23,35],set_variable_text:[4,15,23,35],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,35],setup:[4,9,13,35],sever:[5,22,38],shape:[4,21,31],share:[4,19],sheet:[4,35],shop:4,short_item_text:35,shorten:[4,35],shorthand:[4,5],shortnam:[5,7],should:[3,4,10,14,21,22,26,34,35,38,39],show:[4,9,13,19,21,22,31,35],shown:[4,9,34,35],side:[8,35],sig:[5,9,10,14,35],siglevel:35,signific:[5,9,11,27],significancetest:35,sigproperti:35,similar:[28,38],similarli:[22,23],similiar:23,simpl:[5,6,9,19,25,35,37],simpli:[4,22,23,24,25,31,34,35],simplifi:[24,26],sinc:[9,26,31,39],singl:[3,4,10,19,20,21,22,23,24,26,34,35,37,39],sit:26,six:22,size:[5,7,9,21,22,31,35],skip:[22,23,31,35],skip_item:35,slice:[4,7,15,18,35],slicer:[4,18,22,24,35],slicex:4,small:[5,35],snack:22,snapshot:[4,21,35],snowboard:[19,24,39],soccer:24,social:0,softwar:[0,4,5,9,20,34],solut:35,solv:35,some:[13,14,15,22,25,26,34,35,38],someth:38,sometim:[21,28,34],soon:35,sorri:0,sort:[4,15,35],sort_by_weight:4,sortx:4,sourc:[0,4,10,19,20,22,23,35,39],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:7,specfic:14,special:[0,11,14,19,28,31,35],specif:[3,4,5,7,9,10,11,13,14,15,19,21,23,35,39],specifi:[2,4,5,6,9,10,13,20,23,31,35,37],speed:35,spell:[4,34],split:[4,10,13,35],split_view_nam:10,sport:[20,22],spreadsheet:0,spss:[0,4,9,18,35],spss_limit:[4,35],squar:10,stack:[2,3,4,5,7,11,13,14,27,31,35],stage:[4,35],standalon:18,standard:[7,9,20,34],standardli:24,start:[4,18,23,24,26],start_meta:[4,35],start_tim:22,stat:[4,5,9,10,31,35],state:[4,15,18,24,35],statement:[4,5,19,25,26],statisfi:35,statist:[0,4,5,7,9,10,18,27,28,35],std_paramet:7,stddev:[9,10,31,35],ste:35,stem:35,step:[31,37],still:[26,35],store:[4,5,10,11,12,13,19,21,24,28,31,35],store_cach:10,str:[3,4,5,6,7,8,9,10,24,34,35,37,38],str_tag:[4,34],strict:[4,35],strict_select:8,strictli:23,string:[2,4,6,7,9,10,19,20,21,22,23,24,25,34,35],strip:35,structur:[0,4,6,8,10,11,13,19,20,21,24,28,35,38],studi:35,style:[4,10],sub:[10,31],subclass:[2,11],subclasss:15,sublist:35,subset:[4,8,18,22,24,35],subset_d:4,substr:[4,34],subtl:35,subtyp:[19,35,39],suffix:[4,5,24,34,35,39],sum:[4,5,7,9,10,27,35],summar:[4,5,9,35],summari:[4,5,6,7,9,10,13,22,31,34,35],summaris:10,summat:9,suppli:24,supporintg:10,support:[0,7,10,18,19,22,23,24,34,35,39],surf:[19,24,39],survei:21,sv_se:[31,35],swap:[7,9,10,35],swedish:[19,35],swim:24,syntax:35,sys:4,tab:20,tabl:[0,10],tabul:[5,13,28],tag:[4,23,34,35],take:[4,5,10,11,22,24,25,26,34,35,38],taken:[4,10,14,15,24,34,35],target:[4,6,18,23,34,35],target_item:4,task:22,team:22,temp:4,templat:[5,8,35],temporari:[4,35],temporarili:4,ten:26,tend:4,term:[10,23,35],termin:35,test:[2,4,5,7,9,11,22,27,34,35,37],test_cat_1:37,test_cat_2:37,test_cat_3:37,test_tot:[5,9,35],test_var:[34,35,37],testtyp:9,text1:20,text:[4,5,7,10,14,18,20,21,22,24,26,31,35,37,39],text_kei:[3,4,10,11,18,22,23,31,35],text_label:[4,35],text_prefix:10,textkei:[4,35],than:[4,22,24,34,35],thei:[4,9,13,14,20,25,26,31,34,35],them:[4,5,13,20,22,26,31,34,35],themselv:[4,9],therefor:[4,5,24,34,35],thi:[2,3,4,5,6,9,10,13,14,15,20,21,22,23,24,26,28,31,35,38,39],third:18,thorugh:24,those:4,three:[4,21,22,24,26,35,37],threshold:5,through:[2,3,4,8],throughout:[4,19,20,35],thu:6,time:[10,19,21,22],titl:13,tks:35,to_arrai:[4,34,35,38],to_delimited_set:[4,35],to_df:9,to_excel:10,todo:[4,5,6,8,9,10],togeth:[3,4,10,19,21],toggl:10,too:35,tool:[5,20,24,25],top2:31,top3:10,top:26,topic:[19,34,35],total:[4,5,6,9,13,35],toward:35,tracker:35,tradit:9,transfer:35,transform:[0,4,5,9,18,35],translat:23,transpos:[4,13,24,31,39],transpose_arrai:13,transposit:24,treat:[4,9,25,31],tree:28,treshhold:9,trigger:4,tstat:9,tupl:[4,7,23,24,35,37],turn:19,two:[4,5,9,13,19,21,23,28,31,34,35],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,34,35,37,39],type_nam:10,typic:26,ultim:4,unabbrevi:25,unattend:4,unbind:4,uncod:[4,35],uncode_seri:4,uncodit:5,unconditi:9,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:10,uni:[5,9],unicod:10,unifi:[4,35],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,10,24,28,31,35],unique_id:[4,22],unique_kei:[4,35,38],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:9,unqiu:24,unrol:[4,31,35],untouch:[23,34],unusu:35,unwant:[4,35],unweight:[9,10,31,35],unweighted_bas:[10,31,35],unwgt:35,upcom:33,updat:[4,7,8,9,23,34,35],update_axis_def:[9,35],update_exist:[4,35],upon:19,upper:[4,35],upper_q:9,uppercas:34,usag:[23,34,35],use:[0,2,4,5,9,10,12,13,19,20,21,22,23,24,26,34,35,36],use_ebas:9,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,34,35],useful:[21,22,35],user:[2,4,14,35,37],userwarn:[35,37],uses:[4,9,35],using:[0,2,3,4,6,10,19,20,21,24,25,26,28,31,34,35,39],usual:19,utf8:34,utf:10,val:4,val_text_sep:4,valid:[4,6,10,14,19,24,26,31,35,37],valid_cod:35,valid_tk:[11,35],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,34,35,36,39],value_count:[4,22,35],value_map:38,value_text:[4,22,35],valueerror:[4,21,23,34,35],var_exist:[22,35],var_grid:38,var_nam:[35,38],var_suffix:4,varcoeff:9,vari:22,variabl:[0,4,5,6,7,9,10,11,18,19,20,23,27,34,35,37,38,39],variable_typ:10,variables_from_set:35,varianc:9,variant:[22,34],varibal:38,varibl:35,variou:[5,14,22,28,31],varlist:[4,35],varnam:[4,35],vector:9,verbatim:[11,34],verbos:[4,10,25,31,35],veri:[19,23,24,35],versa:9,version2:24,version:[4,5,9,10,19,21,23,24,26,34,35],versu:31,vertic:[4,18],via:[0,4,5,10,21,22,23,24,31,35],vice:9,view:[1,2,3,4,5,8,9,10,14,16,22,27,28,35],view_kei:31,view_name_not:9,viewmanag:35,viewmapp:[1,10],viewmeta:7,visibl:[31,35],vmerg:[4,34,35],wai:[10,12,13,19,21,22,23,26,31,35,36],wait:21,want:[21,24,26],warn:[4,31,34,35],water:38,wave:21,weak:[4,34],weak_dup:4,week:22,weight:[0,4,6,7,9,10,11,12,22,24,31,34,35],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,weigth:4,well:[4,9,20,22,25,26,31,34,35,39],went:35,were:[26,34,35],wgt:35,what:[16,19,20,24,26,27,35,36,39],whatev:[4,26],when:[4,5,9,10,20,21,23,24,26,35,39],where:[2,3,4,9,24,25,26],whether:[5,9],which:[4,5,9,10,11,13,14,15,22,23,24,26,28,31,34,35,38],whole:[4,35],whose:[4,10,35],wide:35,wil:4,wildcard:26,window:20,windsurf:[19,24,39],wise:[4,31],witch:35,within:[4,9],without:[34,35],women:15,work:[4,11,21,23,31,35],workbook:3,workspac:35,world:20,would:[4,19,24,26,35],wouldn:[19,24],wrap:35,wrapper:[4,9,34],write:[4,10,20,21,35,39],write_dimens:[4,34,35],write_quantipi:[4,21],write_spss:[4,20],writen:38,written:[21,35],wrong:35,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:9,xks:[4,10,31,35],xlsx:4,xml:[4,20],xsect:9,xtotal:4,y_filter:35,y_kei:[14,28,31],y_on_i:[13,28,31,35],year:[19,22,39],yes:20,yet:34,yield:22,yks:[4,35],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,35,38,39],younger:24,your:[4,19,20,21,24,26,35],ysect:9},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","View","ViewMapper","quantify.engine","Stack","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Documentation","Release notes","Upcoming (September)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,39],Using:20,about:38,access:39,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:35,arrai:[13,19,24,38,39],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,37],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:39,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,37,39],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,39],datafram:20,dataset:[4,15,19,21,23,38,39],deciph:20,definit:31,deriv:26,derot:38,descript:31,detail:26,dice:22,differ:37,dimens:[20,39],document:32,edit:23,end:13,engin:[9,29],exampl:26,exist:[22,25],extend:23,featur:0,fill:26,fillna:26,filter:[14,21],from:[20,23],has_al:25,has_ani:25,has_count:25,horizont:21,how:[36,38],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:34,link:28,list:25,load:12,logic:25,manag:21,map:19,mapper:26,mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:39,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[33,35],numer:26,object:[19,22,23],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,process:18,properti:14,python:0,quantifi:9,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,releas:[33,35],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:34,set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:36,special:13,spss:20,stack:[10,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:34,use:38,valu:[19,22,23,37],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[7,17,31],viewmapp:8,wai:37,weight:14,what:[17,28,38]}}) \ No newline at end of file +Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/Stack","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/lib_doc/overview","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:52,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\Stack.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\lib_doc\\overview.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],drop_duplicates:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],min_value_count:[4,2,1,""],names:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],resolve_name:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],unbind:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[10,2,1,""],count:[10,2,1,""],exclude:[10,2,1,""],filter:[10,2,1,""],group:[10,2,1,""],limit:[10,2,1,""],normalize:[10,2,1,""],rebase:[10,2,1,""],rescale:[10,2,1,""],summarize:[10,2,1,""],swap:[10,2,1,""],unweight:[10,2,1,""],weight:[10,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[7,2,1,""],add_link:[7,2,1,""],add_nets:[7,2,1,""],add_stats:[7,2,1,""],add_tests:[7,2,1,""],aggregate:[7,2,1,""],apply_meta_edits:[7,2,1,""],cumulative_sum:[7,2,1,""],describe:[7,2,1,""],freeze_master_meta:[7,2,1,""],from_sav:[7,3,1,""],load:[7,3,1,""],recode_from_net_def:[7,3,1,""],reduce:[7,2,1,""],refresh:[7,2,1,""],remove_data:[7,2,1,""],restore_meta:[7,2,1,""],save:[7,2,1,""],variable_types:[7,2,1,""]},"quantipy.Test":{get_se:[10,2,1,""],get_sig:[10,2,1,""],get_statistic:[10,2,1,""],run:[10,2,1,""],set_params:[10,2,1,""]},"quantipy.View":{get_edit_params:[8,2,1,""],get_std_params:[8,2,1,""],has_other_source:[8,2,1,""],is_base:[8,2,1,""],is_counts:[8,2,1,""],is_cumulative:[8,2,1,""],is_meanstest:[8,2,1,""],is_net:[8,2,1,""],is_pct:[8,2,1,""],is_propstest:[8,2,1,""],is_stat:[8,2,1,""],is_sum:[8,2,1,""],is_weighted:[8,2,1,""],meta:[8,2,1,""],missing:[8,2,1,""],nests:[8,2,1,""],notation:[8,2,1,""],rescaling:[8,2,1,""],spec_condition:[8,2,1,""],weights:[8,2,1,""]},"quantipy.ViewMapper":{add_method:[9,2,1,""],make_template:[9,2,1,""],subset:[9,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[10,1,1,""],Rim:[6,1,1,""],Stack:[7,1,1,""],Test:[10,1,1,""],View:[8,1,1,""],ViewMapper:[9,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":39,"boolean":[4,6,7,18,35],"case":[0,4,5,6,7,10,12,13,15,18,20,23,24,26,28,34,35,37,38],"class":[2,3,4,5,6,7,8,9,10,11,22,34,35],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,34,35,38,39],"export":[0,3,4,20,35],"final":26,"float":[4,5,7,10,19,20,21,22,23,24,31,34,35],"function":[4,7,20,25,26,28,31,34,35],"import":[4,10,11,15,20,22,25,26],"int":[4,5,7,10,19,20,21,22,23,24,25,34,35],"long":[25,34,35],"m\u00fcller":0,"new":[3,4,5,7,9,10,12,21,23,24,26,31,34,35,38,39],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,34,35],"short":[9,19,21,24,35],"sigur\u00f0sson":0,"static":[2,3,4,7],"switch":[24,34,38],"true":[4,5,6,7,9,10,13,20,21,22,24,25,26,31,34,35,38,39],"try":[4,7,23,35],"var":[4,10,15,34,35,38],"while":[4,15,19,23,24,34,35],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31,34],NPS:31,Not:[19,24,31,34],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,34,35,36,37,38],Their:5,Then:[21,35],There:[13,19,21,22,31,35],These:[15,28,31,35],Use:[4,10,24,35],Uses:4,Using:[7,18,21,23],Will:[4,5,7,10,35],With:[13,22,23,24,26,31],Yes:[20,35],__init__:[12,35],__setitem__:35,_band:4,_batch:[7,31],_cumsum:[31,35],_data:[4,11,21],_dimensions_suffix:35,_get_chain:35,_grid:[35,38],_intersect:[28,31],_meta:[4,11,12,13,22,28,39],_missingfi:10,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:35,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,35],about:[13,19,22,36],abov:[24,31,39],absorb:35,accept:[23,26,35],access:[4,7,19,20,22,35,36],accessor:22,accid:[21,35],accommod:14,accompani:[19,35],accord:[4,7,10,26,31],accordingli:26,account:[4,5,34,35],achiev:21,across:[10,22,35],act:[5,24,28],activ:[19,20,24,35],add:[3,4,5,7,9,13,14,23,24,28,31,34,35,37],add_batch:[12,31],add_chain:3,add_data:7,add_filt:[14,35],add_group:6,add_i:13,add_link:[7,28,31],add_meta:[4,23,34,35,37,39],add_method:9,add_net:[7,31,35],add_open_end:[13,34,35],add_stat:[7,31,35],add_test:[7,31,35],add_tot:35,add_x:[13,31],add_y_on_i:[13,28,31,35],adddit:20,added:[4,7,12,13,14,23,24,28,31,34,35,38],adding:[4,14,18,20,31,35,39],addit:[4,13,19,22,31,34,35],addition:[19,34,35],adjust:[34,35],adopt:13,aerob:24,affix:7,after:[4,7,10,21,26,28,31,34,35],afternoon:22,again:[22,23,31],against:[4,5,10,13,35],age:[4,13,14,22,24,26,28,31,35],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,35],agg:[8,13,31],aggnam:8,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,34,35],aim:[0,35],alasdair:0,alert:34,alexand:0,algorithm:[4,5,10,35],alia:[4,21],align:35,all:[2,3,4,5,7,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,34,35,37,38],allign:35,allow:[4,14,15,23,24,31,34,35],alon:[22,35],along:[2,10],alongsid:[7,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,35],also:[4,7,10,11,12,13,14,22,23,24,26,28,31,34,35,37,38],altern:[4,23,31,37],although:22,alwai:[4,15,23,24,26,31,35,39],amount:[4,14,21,28,31],analysi:[0,35],ani:[4,5,7,8,10,13,19,21,22,23,25,26,34,35],anim:[4,19],anoth:[7,13,23,24,26,35],answer:[4,7,10,19,22,23,24,35],anymor:[34,35],anyth:26,anywher:7,api:4,appear:[4,35,37],append:[4,5,18,31,38],appli:[4,5,7,9,10,15,20,21,22,24,28,31,35,39],applic:[4,24,26],apply_edit:4,apply_meta_edit:7,apporach:21,approach:37,appropri:[4,5],arab:35,arbitrari:7,arbitrarili:[4,21,25,26],archiv:33,argument:[5,9,11,20,23,24,35],aris:28,arithmet:10,around:[4,35],arrai:[4,7,10,11,18,21,22,23,31,34,35,36,37],array_item:35,array_var:39,array_var_1:39,array_var_2:39,array_var_3:39,array_var_grid:39,arriv:10,as_addit:[13,34,35],as_delimited_set:35,as_df:10,as_float:35,as_int:35,as_singl:35,as_str:35,as_typ:[4,35],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,10],aspect:35,assess:[0,38],assign:[4,6,35,37],assignd:24,associ:[2,7],assum:[4,10,28,38],attach:[4,7,21,23,35],attempt:[4,34],attribut:[2,4,11,22,28,31,34,35],audit:35,auto:[4,7,34],autom:[0,7],automat:[4,13,14,20,22,23,24,31,34,35,39],auxiliari:10,avail:4,avoid:26,axes:4,axi:[2,3,4,5,7,8,10,15,19,22,35],axis_edit:[4,35],b_d:4,b_filter:34,b_name:31,back:[4,5,38],badli:[4,35],band:[4,7,18,34,35],band_numer:35,bank:3,bank_chain:3,bar:35,base:[4,5,7,8,9,10,18,20,22,31,35],base_al:10,base_text:15,based_on:[4,35],baselin:9,basi:26,basic:[20,22,25,27,35],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,7,15,28,31,34,35],batch_nam:[4,7],batchnam:[11,13],bchain:3,becaus:[24,26,35],becom:[4,10,26,35],been:[4,7,10,13,21,24,26,34,35,39],beer:38,befor:[4,7,10,13,14,24,26,31,35],begin:[4,26],behaviour:[5,8,22,26,35,39],being:[4,24,39],belong:[4,11,12,13,31,34,35,38],below:[4,10,13,26,35],benefici:21,benefit:26,better:28,between:[4,10,13,31,35],bia:10,big:21,binari:35,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,blacklist:[4,35],blank:37,blueprint:35,board:[19,24,39],bool:[4,5,7,9,10,22,38],border:4,both:[4,7,12,13,19,20,21,23,24,26,28,34,35,39],bottom3:7,bottom:35,bound:35,bracket:[7,35],brand:38,break_bi:[13,34],breakfast:22,brief:35,broader:19,buchhamm:0,bug:35,bugfix:[34,35],build:[0,3,4,5,10,15,18,19,35],built:[14,31],bunch:34,by_nam:[4,35],by_typ:[4,21,22,35],bytestr:2,cach:[4,7],calc:[5,7,8,10,31,35],calc_onli:[5,31],calcul:[4,5,7,10,14,27,34,35],call:[4,7,21,22,24,28,34,35,39],came:35,can:[2,4,5,7,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,34,35,37,38,39],cannot:[26,34,35],cap:6,carefulli:4,carri:5,case1:38,case2:38,case3:38,casedata:[4,20],cast:35,cat:[4,19,23,34,37,39],cat_nam:[7,35],categor:[4,5,7,18,22,23,27,34,35,36],categori:[4,5,7,19,22,23,34,35,37],categorized_nam:[4,35],caught:35,caus:[34,35],caution:4,cave:[19,24,39],cbase:[7,31,35],cbase_gross:35,cell:[5,10,11,26,35],cell_item:14,cellitem:35,central:5,certain:[4,22,35],chain:[0,1,3,16,22,35],chainmanag:34,chainnam:2,chang:[4,6,7,10,18,21,24,34,35],charact:[4,23,25],characterist:[19,24],chart:[0,35],check:[4,5,7,10,22,26,34,35],check_dup:35,checking_clust:7,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,7,21,24,35],clean_text:[23,35],clean_up:[4,35],clear:35,clear_factor:4,client:34,clone:[4,18],close:12,cluster:[0,1,2,7,13,14,15,35],code:[4,5,6,7,8,10,18,19,22,23,24,31,34,35,37,39],code_count:[4,22,25,35],code_from_label:[4,35],code_map:4,codes_from_nam:[4,35],codes_in_data:[4,22,35],cola:38,collect:[4,5,7,11,13,14,19,21,22,24,27,31,35],collect_cod:[7,35],colour:35,coltest:[5,7,9],column:[2,4,5,6,7,8,10,18,20,22,23,24,26,31,34,35,38,39],combin:[4,5,7,16,22,25,26,28,31,35],combind:38,come:35,comma:25,common:[8,19,24,35],commun:7,compabl:4,compar:[4,5,10,34,35],comparison:10,compat:[4,7,20,35,36],compatibilti:39,complet:[4,10,35],complex:[4,5,6,18,19,21,22,26,31,34,35],compli:[4,26],complic:18,compon:[4,5,7,8,11,15,18,21,22,23,24,35],compos:7,compound:3,comprehens:26,compress:7,comput:[0,4,5,7,9,10,27,28,31,35],concat:[2,22,35],concaten:[2,26],concern:31,cond_map:4,condit:[4,8,10,18,21,22,25,34,35],confirm:7,conflict:35,conjunct:35,connect:[4,7,12,13,35,39],consequ:[28,37],consid:[4,5,7,10,24,34,35],consist:[4,10,11,19,21,23,24,35],constitut:13,constrcut:7,construct:[4,11,13,14,19,21,23,28,31,35,39],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,35,38],content:[21,28],context:[19,23],contrast:[19,24],contributor:0,control:[4,5,7,8,10,23,24,26,35],convcrit:6,convent:[4,10,35,39],convers:[4,18,34,35],convert:[0,4,10,20,24,34,35,39],coordin:9,cope:28,copi:[2,4,9,12,18,21,23,26,34,35,39],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,35],copy_not:[4,35],copy_of_batch1:12,copy_onli:[4,35],copy_to:[4,23,35],core:[5,20,22,25,34,35,37],correct:[10,23,35],correctli:[34,35],correspond:[4,5,10,22,23,24,35,37,39],correspons:35,corrupt:34,could:[23,24,26,35],count:[4,5,7,8,10,31,34,35,38],count_not:[4,22,35],count_onli:[4,22,35],counterpart:35,counts_cumsum:[31,35],counts_sum:31,cpickl:2,crash:35,creat:[2,3,4,6,7,10,11,13,14,15,16,18,20,22,24,28,31,34,35,36],create_set:[4,35],creation:[10,18,23,35],cross:[13,28,31,35],crossbreak:28,crosstab:[4,24],crunch:35,csv:[0,4,7,11,18],cum_sum:10,cumul:[7,8,10,27,35],cumulative_sum:[7,31],current:[0,2,4,7,24,35],custom:[4,7,16,18,20,24,34,35],custom_text:[7,31],customis:35,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:10,cycl:[4,9],dai:[22,26],danish:19,data:[2,4,5,6,7,8,10,11,14,15,20,21,23,24,28,31,34,35,36,38],data_df:[4,35],data_kei:[4,7,14,28,35],datafil:[4,35],datafram:[3,4,6,7,8,10,18,19,21,22,23,28,31,34,35],datakei:35,dataset:[0,1,7,11,12,13,18,20,22,24,28,31,34,35,36,37],dataset_left:4,dataset_right:4,datasmoothi:0,datast:34,date:[4,7,19,20,21,22,23,24,35],dates_as_str:20,ddf:[0,4,35],deafult:[5,10],deal:20,decim:4,deciph:[0,18],deck:0,decod:[7,34],decode_str:7,decor:35,deep:4,deepcopi:35,defin:[2,3,4,7,10,13,14,19,21,23,28,31,34,35,37],definit:[2,4,5,7,10,14,19,24,26,27,28,34,35],definiton:[9,19,35],defint:[0,2,4,5,6,7,10,14,19,35],defintit:[21,39],defintiton:23,del:35,deleg:26,delet:[4,7,35],delimied_set:22,delimit:[4,7,19,20,21,22,23,24,25,26,34,35,37],delimited_set:[22,24,35],demograph:21,depend:[5,13,14,34,35],deprec:[7,35],deriv:[4,10,18,21,34,35,37],derivc:9,derive_categor:35,derot:[4,35,36],desc:4,descend:4,describ:[0,2,3,4,7,14,19,21,28,31,35],descript:[4,5,7,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,35,37],detail:[8,18,19],detect:[4,6,34],determin:[4,5,7],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,35],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,35,38],dictat:21,dictionari:[4,6,7,8,9,35],differ:[4,5,7,8,10,13,19,20,23,24,28,31,34,35,36,38],digest:19,dim:[5,10,31],dim_comp:35,dimens:[0,4,5,10,18,35,36],dimension:[4,35],dimensionizing_mapp:4,dimensions_comp:[4,20,35,39],dimlabel:35,dinner:22,direct:26,directli:[2,4,22,35],discret:[7,13],disk:[4,21,35],dispers:5,distinct:[19,31],distribut:[5,10],div:[7,35],dive:[19,24,39],divid:10,dk_filter:4,dms:4,dmsrun:4,doc:[22,35],docstr:6,document:[0,4,7,19,20,23,35],doe:[4,11,23,26,35],doesn:[4,25,26],dog:19,don:[19,22,24,37],done:[4,13,14],doubl:22,down:39,downbreak:[28,31,35],download:39,draft:35,draw:[21,24],drawn:[4,35],drink:38,drink_1:38,drink_2:38,drink_level:38,driven:0,drop:[4,5,7,10,21,35],drop_cod:[7,35],drop_delimit:4,drop_dupl:4,drope:[4,38],dropna:[4,6,35,38],dropx:4,dto:39,dtype:[20,22,26,35],due:34,dump:20,dupe:34,duplic:[4,23,34,35],durat:22,dure:[4,7,10,24],each:[2,4,5,6,7,13,14,19,23,24,28,31,35,38,39],eagleston:0,eas:35,easi:[0,19,22,23,35],easier:[12,22,28,34],easiest:22,easili:[4,12,20,31,35],ebas:[7,35],echo:4,ect:35,edit:[0,4,8,10,13,14,15,18,19,21,24,26,35],edit_param:8,eff:10,effect:[6,10,26,35],effici:35,ein:[19,23],either:[4,5,10,13,19,21,22,23,26,35],element:[4,7,10,19,35,39],eleph:4,els:26,emploi:19,empti:[4,7,22,24,26,34,35],empty_item:[4,35],en_u:7,enabl:[23,35],encod:[4,34,35],encount:20,end:[2,4,7,11,12,34,35],end_tim:22,enforc:35,eng:35,engin:[1,27,34],english:[19,35],enhanc:34,enough:4,enrich:35,ensur:[4,11,35],enter:[4,14],entir:[4,10,20,35],entri:[4,6,7,10,19,26,35],enumer:[4,7,23,24,26,37],environ:4,eponym:35,eqaul:4,equal:[4,10,35],equip:31,equival:[4,39],eras:[4,7],error:[4,10,34,35],escap:4,especi:[22,26,31],estim:10,etc:[4,19,21,28,31,35],ethnic:[13,22,28,31],even:[4,31,35],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,35],exactli:[4,24,25],exampl:[2,4,5,7,11,18,19,20,22,24,25,28,31,35,38,39],excel:[0,3,4,5,35],except:[4,21,35],exchang:[10,23],exclud:[4,5,7,8,10,31,35],exclus:[4,25,35],execut:4,exercis:[22,24],exist:[4,7,10,12,13,18,21,23,24,26,31,34,35],expand:[4,7,8,10,31],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,7,35,37],explor:19,expos:23,express:[4,5,6,7,10,22,25,35],ext_item:[4,35],ext_valu:4,ext_xk:35,ext_yk:35,extend:[4,5,7,13,18,22,31,35],extend_cod:[7,35],extend_filt:14,extend_i:[13,35],extend_item:[4,35],extend_valid_tk:35,extend_valu:[4,23,35,37],extend_x:[34,35],extens:[3,4,7,19,20,35],extra:35,extract:14,extrapol:25,factor:[4,6,7,35],factor_label:[7,35],factormap:4,fail:[4,35],failur:20,fall:[4,5,10],fallback:8,fals:[4,5,7,10,13,20,22,23,24,26,31,34,35],fast_stack_filt:35,favour:10,featur:[19,35],feed:[4,35],feedback:4,femal:[4,22],few:[11,21,22,35,38],figur:10,file:[2,3,4,5,7,11,20,21,22,28,35,39],file_nam:20,filenam:[2,7],fill:[4,18,34,35],fillna:[4,18],filter1:35,filter2:35,filter:[4,6,7,10,11,15,18,28,31,34,35],filter_1:35,filter_2:35,filter_def:6,filter_kei:[7,14],filter_nam:35,find:[4,11,22,28,34,35],find_duplicate_text:4,finish:[4,21,35],finnish:19,first:[4,5,10,13,19,21,26,31,35],fit:[19,23,24],fix:[4,15,35],flag:[4,5,35],flag_bas:10,flat:4,flatten:[4,24,35],flexibl:[6,7,24],float64:20,folder:20,follow:[4,8,10,11,14,15,19,20,21,22,23,24,26,28,31,35,38,39],folow:26,footbal:24,forc:[26,35],force_text:[4,23,35],forcefulli:4,form:[3,4,7,10,14,20,23,24,25,26,28,35],format:[0,4,6,10,19,20,23,26,31,35,38],former:[4,7,22,23,24,35],fortnight:22,found:[2,3,4,7,10,20,22,24,26,31,34,35],four:[28,31],frang:[4,25,26,35,38],freez:7,freeze_master_meta:7,french:19,frequenc:[4,5,8,9,10,28,31,35],freysson:0,from:[0,2,3,4,5,7,10,12,14,15,18,19,21,24,25,26,28,31,34,35,38],from_batch:[4,35],from_compon:[4,20,35],from_dichotom:[4,35],from_excel:[4,35],from_sav:7,from_set:[4,20,35],from_stack:[4,35],front:35,fulfil:4,full:[3,4,5,7,21,23,25,35,39],fullnam:31,fully_hidden_arrai:4,fun:35,further:21,futur:[7,35],geir:0,gender:[4,13,14,15,22,24,26,28,31,35,38],gener:[2,4,5,7,8,9,11,20,22,23,26,31,35],generate_report:35,german:[19,23],get:[4,7,8,11,12,13,14,22,28,31,35,37],get_batch:[4,12,13,28,31,34],get_edit_param:8,get_properti:4,get_qp_dataset:35,get_s:10,get_sig:10,get_statist:10,get_std_param:8,getter:34,give:[13,26,35],given:[4,5,6,7,10,20,26,34,35],global:[4,8,10,14,15,21,35],goe:4,going:37,grab:34,greater:4,grid:[4,35],griffith:0,group:[4,5,6,7,8,10,19,21,22,24,25,28,31,35],group_nam:6,group_target:6,grouped_ag:24,grp:10,grp_text_map:31,guid:35,gzip:7,hack:4,had:26,hand:24,handl:[0,6,7,9,10,20,31,34,35,37,39],handler:35,happen:[4,24],happend:4,has:[2,4,7,12,15,19,21,25,26,31,34,35,38],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:8,have:[4,7,10,13,20,23,24,25,26,28,34,35,38],head:[20,22,23,24,26,35,39],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,7,26,28,35,39],hidden:[4,34,35],hide:[4,15,34,35],hide_empti:35,hide_empty_item:[4,35],hide_on_i:4,hide_valu:[4,35],high:[5,10],higher:10,hmerg:[4,34,35],hockei:24,hold:[2,4,7,10,34],horizont:[4,18],household:20,how:[3,4,7,14,19,22,24,28,31,37,39],howev:[23,24,26,37,39],hrafn:0,html:[4,23,35],hub:34,ident:[4,20,24,34,35],identif:4,identifi:[4,5,23,35,38],ids:4,ignor:[4,7,10,22,24,35],ignore_arrai:4,ignore_cod:4,ignore_flag:10,ignore_item:[4,24,35],ignore_valu:[4,24],ill:23,implement:[7,10,19,22,35],impli:4,implicitli:10,impract:28,impute_method:6,incl:[7,35],includ:[2,3,4,5,7,10,13,25,26,28,31,34,35,38],inclus:[24,35],incom:[4,10,24],inconsist:[4,15,23,34,35],incorrect:35,incorrectli:35,independ:[4,10],index:[0,2,4,7,10,35],indic:[4,5,7,10,23,24,26,35],individu:[3,4,35],industri:20,infer:[7,20,24,35],info:[4,18,19],inform:[0,4,8,9,13,14,15,19,20,23,26,31,35,37,38],inherit:[11,35],inhomogen:10,init:26,initi:[4,18,35,38],inject:[4,26],innermost:8,inplac:[4,7,10,18,21,26,35],input:[4,6,7,10,11,20,35],insert:[4,13,35],insid:[2,4,9,10,19,20,22,23,35],inspect:[4,18,35],instal:[4,35],instanc:[2,3,4,7,9,10,11,14,20,21,22,23,24,28,31,34,35],instead:[4,7,10,20,25,26,34,35],instruct:[5,10,19,26,31,34,35],int64:[20,22],integ:10,integr:22,intend:7,inter:[4,6],interact:[2,3,4,21,22,35],interfac:0,interim:35,interlock:[4,18,35],intern:[2,34,35],interpret:[4,25],intersect:[4,18,22,35],intro:21,introduc:10,involv:26,iolocal:[7,20],ioutf8:7,ipython:[4,21,35],is_arrai:[31,35],is_bas:8,is_block:31,is_count:8,is_cumul:8,is_dat:35,is_delimited_set:35,is_float:35,is_g:[4,25,35],is_int:35,is_like_numer:[4,22,24,35],is_meanstest:8,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:8,is_pct:8,is_propstest:8,is_singl:35,is_stat:8,is_str:35,is_sum:8,is_weight:[8,31],isol:35,issu:[20,34,35],ist:[19,23],item:[4,10,11,13,19,20,21,22,23,24,31,35,37,38,39],item_nam:[4,35],item_no:[4,35],item_text:[4,22,35],iter:[9,22,35],its:[2,4,5,7,8,10,11,12,13,14,15,19,20,22,24,25,26,31,35,38,39],itself:[4,10],jame:0,jjda:20,jog:24,join:[4,22],json:[4,7,11,18,35],jupyt:[4,21,35],just:35,keep:[4,9,10,21,23,24,35,38],keep_bas:10,keep_cod:[10,35],keep_origin:4,keep_variable_text:[4,35],kei:[2,4,5,7,8,11,14,19,23,24,26,31,35,38],kept:[4,10,11,35],kerstin:0,keyword:[5,9,35],kind:[22,24,28,35],kite:[19,24,39],know:[14,19,22,24,37,39],kritik:31,kwarg:[4,5,8,9,35],lab:7,label:[4,5,7,14,19,20,22,23,24,26,34,35,37,38,39],lack:0,lang:19,languag:[11,18,23,35],larg:[14,21,28,31,35],last:4,lastli:34,later:[4,13,20,35],latest:[0,33,35],latter:[22,35],lead:[24,31,35],least:[4,22,23],leav:[23,24],left:[4,10,34,35],left_id:4,left_on:[4,35],legaci:4,lemonad:38,length:[4,35],less:[22,34,38],let:[23,24,28],level:[4,5,7,10,14,19,26,31,35,38],lib:[4,19,35,39],librari:[0,19,35],lift:24,like:[0,4,5,7,13,14,19,21,22,24,26,28,31,34,35,38,39],limit:[4,10,24,26,35],link:[2,4,5,7,8,9,10,11,14,27,31,35],list:[2,4,5,6,7,9,10,13,18,19,21,22,23,24,26,34,35,37,38],list_vari:35,listen:26,load:[2,3,4,7,11,35],load_cach:7,loc:31,local:[7,13,22,28,31],locat:[2,4,7,35],logic1:35,logic2:35,logic:[4,5,8,10,18,22,24,26,34,35],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:35,look:[4,13,24,26,34,35,38,39],loop:[35,38],lose:[13,24,38],lot:[4,21,22,26],low:[4,5,10,34],lower:[4,10,34,35],lower_q:10,lunch:22,machin:[4,20,35],made:2,mai:[4,25,26],main:[4,6,10,13,14,19,24,31,34,35],main_filt:35,mainli:3,major:10,mak:35,make:[4,5,22,24,28,35],make_summari:[13,35],make_templ:9,male:[22,26],manag:[0,18,26],mani:[13,28],manifest:4,manipul:[10,13,15],manual:[4,5,35,37],map:[4,5,6,7,9,10,18,20,23,35,37],mapper:[4,18,25,35,38],mapper_to_meta:4,margin:[5,10],mark:10,market:[0,20],mask:[4,18,21,22,23,35,37,38,39],mass:5,massiv:26,master:[34,35],master_meta:7,match:[4,6,7,20,24,35,39],matric:10,matrix:10,matrixcach:7,matter:[11,34],max:[7,10,31,35],max_iter:6,mdd:[0,4,20,35],mdm_lang:35,mean:[4,5,6,7,8,10,13,15,20,22,24,25,26,31,35,38],measur:[5,10],median:[6,7,10,31,35],membership:[22,35],memori:[21,35],memoryerror:35,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,34,35],merge_exist:[4,34],merge_text:[4,35],messag:35,meta:[4,5,7,8,11,12,14,15,18,20,21,22,24,26,31,34,35,37,38,39],meta_dict:[4,35],meta_edit:[4,7,15],meta_to_json:[4,35],metadata:[0,4,7,18,19,20,24,26,34,35,37,39],metaobject:35,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,34,35,37,38,39],metric:[5,7,10],mid:[5,10,22,35],middl:35,might:[7,21,23,24,34],mimic:[5,10],mimick:[5,10,22],min:[4,7,10,31,34,35],min_value_count:[4,34],minimum:5,minor:35,mismatch:35,miss:[4,6,8,14,20,22,23,24,34,37,39],missing_map:[4,35],mix:[4,24],mode:[6,7,20,35,36],modifi:[4,7,10,15,24,34,35],modu:35,modul:[0,4,7],month:22,more:[2,4,22,25,26,31,34,35],morn:22,most:[10,21,22,26,34],mous:4,move:[0,4,21,35],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,38],mul:7,multi:[5,10,19],multiindex:10,multipl:[0,3,4,5,19,22,23,25,26,35],multipli:10,multivari:10,must:[4,6,7,20,21,23,26,31,35,38],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,34,35,37,38,39],name_data:[35,39],nan:[4,10,20,22,23,24,26,28,31,35,39],nate:4,nativ:[0,4,18,35],natur:[10,21,22],necessari:[10,21,35,38],need:[4,7,9,10,21,23,24,34,35,39],neg:35,nest:[7,8,19,25,26],net:[5,7,8,10,18,27,34,35],net_1:[7,31],net_2:[7,31],net_3:7,net_map:[7,31],net_view:35,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:35,new_cod:10,new_column:4,new_d:35,new_data:7,new_data_kei:7,new_dataset:4,new_int:23,new_meta:7,new_nam:[4,23,24,34,35],new_ord:[4,21,35],new_rul:35,new_set:[4,35],new_singl:23,new_stack:2,new_text:[4,23],new_var:35,new_weight:7,newli:34,next:[4,7,11,19,31,35,39],no_data:24,no_filt:[4,7,28,31,35],non:[4,7,22,25,27,35],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,34,35,37,39],none_band:35,nonea:4,normal:[4,5,7,10,35],norwegian:19,not_al:18,not_ani:[4,10,18,35],not_count:18,notat:[5,7,8,10],note:[2,4,5,26,31],notebook:[4,21,35],notimplementederror:[15,35],now:[13,14,21,31,34,35,38],num:5,number:[4,6,10,20,22,23,24,34,35],numer:[4,5,7,18,19,23,24,31,34,35,37],numpi:[0,10,35],obei:22,object:[2,3,4,5,7,10,18,20,21,24,26,28,31,35,37],obscur:26,observ:0,obvious:31,occur:26,oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,35,37,39],often:[21,22,24],old:[4,5,7,35],old_cod:10,old_nam:35,older:35,omit:26,omnibu:34,on_var:[7,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,34,35],ones:[4,10,15,21,22,34],onli:[4,7,9,10,11,13,14,21,23,24,26,28,31,34,35,37,38],only_men:24,only_typ:[4,7],onto:26,oom:35,open:[0,11,12,34,35],oper:[4,5,20,22,24,25,26,35],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,7,10,13,14,19,22,23,31,34,35],order:[2,4,11,18,19,24,34,35],ordereddict:[3,4,7,13,28,31],organ:16,orgin:10,orient:[2,35],origi:4,origin:[4,7,10,21,24,34,35],other:[4,5,7,10,11,12,13,19,23,24,25,26,34,35,38],other_sourc:[7,31,35],otherwis:[4,34,35],our:[0,22,26],out:[5,12,14,24,31,35],outcom:4,outdat:35,output:[4,7,10,13,22,35],outsid:35,over:[4,7,9,10,35],overcod:[4,7,31],overlap:10,overview:[4,7,22,28,35],overwrit:[4,15,21,26,34,35],overwrite_margin:10,overwrite_text:4,overwritten:[4,7,35],ovlp_correc:10,own:[13,35],pack:4,packag:5,paint:[3,35],painter:35,pair:[4,5,10,18,23,26,35,37],panda:[0,3,4,6,7,10,19,20,22,26,35],pane:4,parachut:[19,24,39],parallel:7,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,34,35,38],parent:[4,19,20,35],pars:5,part:[4,8,19,21,22,26,35],parti:18,particip:20,particular:4,pass:[4,5,7,10,21,22,24,34,35],past:26,path:[2,3,4,7,20,35],path_clust:3,path_csv:20,path_data:[4,39],path_ddf:[4,20,35],path_json:20,path_mdd:[4,20,35],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:7,path_txt:20,path_xlsx:[4,35],path_xml:20,pct:4,peopl:20,per:[4,5,6,7,10,12,20,22,23,24,26,35,39],percentag:[7,8,10,31,35],perform:[4,5,7,8,10,21,26,31,34,35],perman:21,physic:35,pick:[4,24],pickl:2,pilat:24,pivot:7,place:[10,26,34,35],plai:22,plain:[0,8,20],plan:[11,13,14,19,24,28,31],pleas:[7,21,22,34,35],point:19,pointer:19,pool:10,popul:[4,11,13,14,27,31,35],portion:7,posit:[4,10,21,22,23,28,34,35],possibl:[3,4,5,7,12,13,19,22,23,25,31,34,35,38],power:0,powerpoint:[5,35],powerpointpaint:35,pptx:35,pre:[4,26,31],precis:[26,35],prefer:22,prefix:[4,7],prep:25,prepar:[3,21,23,31,34,35],present:[4,10,35],preset:[4,7],pretti:[4,26],prevent:[4,15,21,23,24,34,35],previou:[21,35],previous:[4,35],primarili:4,print:[13,22,28,31,35],prior:[4,24],prioriti:35,probabl:[19,24,35],problem:[34,35],process:[0,4,7,9,20,21,22],produc:[5,10,13,24],product:[35,38],profession:[4,35],progress:[4,35],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[34,35,39],properli:39,properti:[4,10,11,20,35],proport:[5,6,8],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,35,37],proxi:7,purpos:19,put:7,python:[4,34],q01_1:4,q01_3:4,q11:35,q11_grid:35,q12:38,q12_10:38,q12_11:38,q12_12:38,q12_13:38,q12_1:38,q12_2:38,q12_3:38,q12_4:38,q12_5:38,q12_6:38,q12_7:38,q12_8:38,q12_9:38,q12_:38,q12a:38,q12a_10:38,q12a_11:38,q12a_12:38,q12a_13:38,q12a_1:38,q12a_2:38,q12a_3:38,q12a_4:38,q12a_5:38,q12a_6:38,q12a_7:38,q12a_8:38,q12a_9:38,q12a_grid:38,q12b:38,q12b_10:38,q12b_11:38,q12b_12:38,q12b_13:38,q12b_1:38,q12b_2:38,q12b_3:38,q12b_4:38,q12b_5:38,q12b_6:38,q12b_7:38,q12b_8:38,q12b_9:38,q12b_grid:38,q12c:38,q12c_10:38,q12c_11:38,q12c_12:38,q12c_13:38,q12c_1:38,q12c_2:38,q12c_3:38,q12c_4:38,q12c_5:38,q12c_6:38,q12c_7:38,q12c_8:38,q12c_9:38,q12c_grid:38,q12d:38,q12d_10:38,q12d_11:38,q12d_12:38,q12d_13:38,q12d_1:38,q12d_2:38,q12d_3:38,q12d_4:38,q12d_5:38,q12d_6:38,q12d_7:38,q12d_8:38,q12d_9:38,q12d_grid:38,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,38],q1_2:[4,26,38],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:35,q4a_1:35,q4a_2:35,q4a_3:35,q4a_grid:35,q5_1:[19,21,22,24,35,39],q5_2:[19,21,22,24,35,39],q5_3:[19,21,22,24,35,39],q5_4:[19,21,22,24,35,39],q5_5:[19,21,22,24,35,39],q5_6:[19,21,22,24,35,39],q5_grid:39,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:39,q6_net:31,q6copi:39,q6new:39,q6new_grid:39,q6new_q6copi:39,q6new_q6copy_grid:39,q6new_q6copy_tran:39,q6new_q6copy_trans_grid:39,q7_1:[21,22,35],q7_2:[21,22,35],q7_3:[21,22,35],q7_4:[21,22,35],q7_5:[21,22,35],q7_6:[21,22,35],q7_grid:39,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:38,q_label:[4,35],qtp:38,qtype:[4,23,34,37,39],qualifi:[4,5,10],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,35,37,39],quantipyview:[1,7,35],quantiti:[10,35],queri:[2,4,6,7,18,19,24,35,39],question:[4,10,19,24,26,31,35,38],questionnair:21,quick:[4,22,35],quickli:[6,21,22,24,35],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,34,35],rake:35,rang:[4,5,18,21,35],rate:[35,38],raw:[5,10],raw_sum:10,rbase:7,read:[0,4,20,35],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,35,39],read_spss:[4,20],rebas:10,rebuild:24,rec:[4,35],receiv:38,recod:[0,4,7,18,35],recode_from_net_def:7,recode_seri:4,recoded_filt:35,recommend:24,record_numb:[13,22],reduc:[4,7,10,21,35],reduced_d:21,reduct:4,refactor:35,refer:[4,7,10,19,23,26,28,31,35],referenc:[7,13,19,26,35],reflect:[4,10,21,35],refresh:7,refus:[19,24],regard:[4,35],region:[4,35],regist:[4,22,35],regroup:[4,34],regular:[4,19,31,35],regularli:[22,23,24],reindex:4,reintroduc:35,rel:21,rel_to:8,relat:[2,8,23,26,35],relation_str:8,relationship:7,relev:[23,35],religion:22,reload:[21,35],remain:[4,5,21,26,35],rememb:37,remind:37,remov:[4,5,7,10,12,13,18,34,35],remove_data:7,remove_filt:35,remove_html:[4,35],remove_item:4,remove_valu:[4,34],renam:[4,18,24,34,35,39],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,34,35],reorder_item:[4,35],reorder_valu:4,reorgan:0,repair:[4,34,35],repair_text_edit:[4,35],repeat:[21,28],repetit:26,replac:[4,10,13,23,26,31,35],replace_i:[13,35],replace_text:[4,35],report:[0,4,5,6,34,35],reposit:[4,21,35],repres:[4,26],represent:[7,8,10,19,24,34],request:[5,7,13,21,23,26,31,35],request_view:35,requir:[4,21,23,35,39],rescal:[5,7,8,10,31],research:[0,20],reset:[4,35],reset_index:4,resid:35,resolv:34,resolve_nam:[4,34],resp:34,respect:[4,10,24,34,35,37],respond:[10,21,38],respons:[19,22,25,26,35,38],responsess:35,restor:[4,7,21,35],restore_item_text:4,restore_meta:7,restrict:[4,5,7,10,19,21,22,35],result:[3,4,5,7,9,10,16,20,22,23,24,26,28,35],result_onli:10,retain:9,retriev:10,revers:[24,25],revert:[4,21,35],right:[4,34,35],right_id:4,right_on:[4,35],rim:[1,4,35],roll:4,roll_up:4,rollback:[18,35],rolled_up:4,round:6,row:[4,5,10,18,20,22,35,39],row_id:4,row_id_nam:4,rule:[4,35,39],run:[4,7,10,15,24,28,31,35],safe:[4,23],safeguard:4,sai:26,same:[3,4,7,13,19,20,22,26,28,34,35,38,39],sampl:[5,8,10,34,35],sample_s:14,sandbox:35,satisfi:35,sav:[4,7,20],save:[2,3,4,7,21,28,35],savepoint:18,scalar:35,scale:[5,6,10,19,35,38],scan:4,scenario:39,scheme:[4,6,19,35],scratch:[18,35],script:4,search:4,second:[4,5,10,15,31],sect:4,section:[8,10,11,14,21,26],see:[13,21,24,26,28,31,35],seen:[26,39],segemen:26,segment:18,select:[4,7,9,13,14,21,22,31,35,38],select_text_kei:4,self:[2,4,7,10,26,28,35],sem:[7,10],semi:34,sensit:[4,34],separ:[4,26,37],septemb:33,sequenc:4,seri:[4,19,22,26,35],serial:2,session:[21,35],set:[3,4,5,6,7,9,10,11,12,13,18,19,20,22,23,24,26,28,34,35,37],set_cell_item:14,set_col_text_edit:35,set_column_text:35,set_dim_suffix:35,set_encod:4,set_factor:4,set_item_text:[4,23,35],set_languag:14,set_mask_text:35,set_miss:[4,35],set_opt:35,set_param:10,set_properti:[4,15],set_sigtest:[14,31,35],set_target:6,set_text_kei:4,set_unwgt_count:35,set_val_text_text:35,set_value_text:[4,15,23,35],set_variable_text:[4,15,23,35],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,35],setup:[4,10,13,35],sever:[5,22,38],shape:[4,21,31],share:[4,19],sheet:[4,35],shop:4,short_item_text:35,shorten:[4,35],shorthand:[4,5],shortnam:[5,8],should:[3,4,7,14,21,22,26,34,35,38,39],show:[4,10,13,19,21,22,31,35],shown:[4,10,34,35],side:[9,35],sig:[5,7,10,14,35],siglevel:35,signific:[5,10,11,27],significancetest:35,sigproperti:35,similar:[28,38],similarli:[22,23],similiar:23,simpl:[5,6,10,19,25,35,37],simpli:[4,22,23,24,25,31,34,35],simplifi:[24,26],sinc:[10,26,31,39],singl:[3,4,7,19,20,21,22,23,24,26,34,35,37,39],sit:26,six:22,size:[5,8,10,21,22,31,35],skip:[22,23,31,35],skip_item:35,slice:[4,8,15,18,35],slicer:[4,18,22,24,35],slicex:4,small:[5,35],snack:22,snapshot:[4,21,35],snowboard:[19,24,39],soccer:24,social:0,softwar:[0,4,5,10,20,34],solut:35,solv:35,some:[13,14,15,22,25,26,34,35,38],someth:38,sometim:[21,28,34],soon:35,sorri:0,sort:[4,15,35],sort_by_weight:4,sortx:4,sourc:[0,4,7,19,20,22,23,35,39],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:8,specfic:14,special:[0,11,14,19,28,31,35],specif:[3,4,5,7,8,10,11,13,14,15,19,21,23,35,39],specifi:[2,4,5,6,7,10,13,20,23,31,35,37],speed:35,spell:[4,34],split:[4,7,13,35],split_view_nam:7,sport:[20,22],spreadsheet:0,spss:[0,4,10,18,35],spss_limit:[4,35],squar:7,stack:[1,2,3,4,5,8,11,13,14,27,31,35],stage:[4,35],standalon:18,standard:[8,10,20,34],standardli:24,start:[4,18,23,24,26],start_meta:[4,35],start_tim:22,stat:[4,5,7,10,31,35],state:[4,15,18,24,35],statement:[4,5,19,25,26],statisfi:35,statist:[0,4,5,7,8,10,18,27,28,35],std_paramet:8,stddev:[7,10,31,35],ste:35,stem:35,step:[31,37],still:[26,35],store:[4,5,7,11,12,13,19,21,24,28,31,35],store_cach:7,str:[3,4,5,6,7,8,9,10,24,34,35,37,38],str_tag:[4,34],strict:[4,35],strict_select:9,strictli:23,string:[2,4,6,7,8,10,19,20,21,22,23,24,25,34,35],strip:35,structur:[0,4,6,7,9,11,13,19,20,21,24,28,35,38],studi:35,style:[4,7],sub:[7,31],subclass:[2,11],subclasss:15,sublist:35,subset:[4,9,18,22,24,35],subset_d:4,substr:[4,34],subtl:35,subtyp:[19,35,39],suffix:[4,5,24,34,35,39],sum:[4,5,7,8,10,27,35],summar:[4,5,10,35],summari:[4,5,6,7,8,10,13,22,31,34,35],summaris:7,summat:10,suppli:24,supporintg:7,support:[0,7,8,18,19,22,23,24,34,35,39],surf:[19,24,39],survei:21,sv_se:[31,35],swap:[7,8,10,35],swedish:[19,35],swim:24,syntax:35,sys:4,tab:20,tabl:[0,7],tabul:[5,13,28],tag:[4,23,34,35],take:[4,5,7,11,22,24,25,26,34,35,38],taken:[4,7,14,15,24,34,35],target:[4,6,18,23,34,35],target_item:4,task:22,team:22,temp:4,templat:[5,9,35],temporari:[4,35],temporarili:4,ten:26,tend:4,term:[7,23,35],termin:35,test:[2,4,5,8,10,11,22,27,34,35,37],test_cat_1:37,test_cat_2:37,test_cat_3:37,test_tot:[5,10,35],test_var:[34,35,37],testtyp:10,text1:20,text:[4,5,7,8,14,18,20,21,22,24,26,31,35,37,39],text_kei:[3,4,7,11,18,22,23,31,35],text_label:[4,35],text_prefix:7,textkei:[4,35],than:[4,22,24,34,35],thei:[4,10,13,14,20,25,26,31,34,35],them:[4,5,13,20,22,26,31,34,35],themselv:[4,10],therefor:[4,5,24,34,35],thi:[2,3,4,5,6,7,10,13,14,15,20,21,22,23,24,26,28,31,35,38,39],third:18,thorugh:24,those:4,three:[4,21,22,24,26,35,37],threshold:5,through:[2,3,4,9],throughout:[4,19,20,35],thu:6,time:[7,19,21,22],titl:13,tks:35,to_arrai:[4,34,35,38],to_delimited_set:[4,35],to_df:10,to_excel:7,todo:[4,5,6,7,9,10],togeth:[3,4,7,19,21],toggl:7,too:35,tool:[5,20,24,25],top2:31,top3:7,top:26,topic:[19,34,35],total:[4,5,6,10,13,35],toward:35,tracker:35,tradit:10,transfer:35,transform:[0,4,5,10,18,35],translat:23,transpos:[4,13,24,31,39],transpose_arrai:13,transposit:24,treat:[4,10,25,31],tree:28,treshhold:10,trigger:4,tstat:10,tupl:[4,8,23,24,35,37],turn:19,two:[4,5,10,13,19,21,23,28,31,34,35],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,34,35,37,39],type_nam:7,typic:26,ultim:4,unabbrevi:25,unattend:4,unbind:4,uncod:[4,35],uncode_seri:4,uncodit:5,unconditi:10,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:7,uni:[5,10],unicod:7,unifi:[4,35],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,7,24,28,31,35],unique_id:[4,22],unique_kei:[4,35,38],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:10,unqiu:24,unrol:[4,31,35],untouch:[23,34],unusu:35,unwant:[4,35],unweight:[7,10,31,35],unweighted_bas:[7,31,35],unwgt:35,upcom:33,updat:[4,8,9,10,23,34,35],update_axis_def:[10,35],update_exist:[4,35],upon:19,upper:[4,35],upper_q:10,uppercas:34,usag:[23,34,35],use:[0,2,4,5,7,10,12,13,19,20,21,22,23,24,26,34,35,36],use_ebas:10,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,34,35],useful:[21,22,35],user:[2,4,14,35,37],userwarn:[35,37],uses:[4,10,35],using:[0,2,3,4,6,7,19,20,21,24,25,26,28,31,34,35,39],usual:19,utf8:34,utf:7,val:4,val_text_sep:4,valid:[4,6,7,14,19,24,26,31,35,37],valid_cod:35,valid_tk:[11,35],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,34,35,36,39],value_count:[4,22,35],value_map:38,value_text:[4,22,35],valueerror:[4,21,23,34,35],var_exist:[22,35],var_grid:38,var_nam:[35,38],var_suffix:4,varcoeff:10,vari:22,variabl:[0,4,5,6,7,8,10,11,18,19,20,23,27,34,35,37,38,39],variable_typ:7,variables_from_set:35,varianc:10,variant:[22,34],varibal:38,varibl:35,variou:[5,14,22,28,31],varlist:[4,35],varnam:[4,35],vector:10,verbatim:[11,34],verbos:[4,7,25,31,35],veri:[19,23,24,35],versa:10,version2:24,version:[4,5,7,10,19,21,23,24,26,34,35],versu:31,vertic:[4,18],via:[0,4,5,7,21,22,23,24,31,35],vice:10,view:[1,2,3,4,5,7,9,10,14,16,22,27,28,35],view_kei:31,view_name_not:10,viewmanag:35,viewmapp:[1,7],viewmeta:8,visibl:[31,35],vmerg:[4,34,35],wai:[7,12,13,19,21,22,23,26,31,35,36],wait:21,want:[21,24,26],warn:[4,31,34,35],water:38,wave:21,weak:[4,34],weak_dup:4,week:22,weight:[0,4,6,7,8,10,11,12,22,24,31,34,35],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,weigth:4,well:[4,10,20,22,25,26,31,34,35,39],went:35,were:[26,34,35],wgt:35,what:[16,19,20,24,26,27,35,36,39],whatev:[4,26],when:[4,5,7,10,20,21,23,24,26,35,39],where:[2,3,4,10,24,25,26],whether:[5,10],which:[4,5,7,10,11,13,14,15,22,23,24,26,28,31,34,35,38],whole:[4,35],whose:[4,7,35],wide:35,wil:4,wildcard:26,window:20,windsurf:[19,24,39],wise:[4,31],witch:35,within:[4,10],without:[34,35],women:15,work:[4,11,21,23,31,35],workbook:3,workspac:35,world:20,would:[4,19,24,26,35],wouldn:[19,24],wrap:35,wrapper:[4,10,34],write:[4,7,20,21,35,39],write_dimens:[4,34,35],write_quantipi:[4,21],write_spss:[4,20],writen:38,written:[21,35],wrong:35,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:10,xks:[4,7,31,35],xlsx:4,xml:[4,20],xsect:10,xtotal:4,y_filter:35,y_kei:[14,28,31],y_on_i:[13,28,31,35],year:[19,22,39],yes:20,yet:34,yield:22,yks:[4,35],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,35,38,39],younger:24,your:[4,19,20,21,24,26,35],ysect:10},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","Stack","View","ViewMapper","quantify.engine","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Documentation","Release notes","Upcoming (September)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,39],Using:20,about:38,access:39,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:35,arrai:[13,19,24,38,39],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,37],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:39,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,37,39],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,39],datafram:20,dataset:[4,15,19,21,23,38,39],deciph:20,definit:31,deriv:26,derot:38,descript:31,detail:26,dice:22,differ:37,dimens:[20,39],document:32,edit:23,end:13,engin:[10,29],exampl:26,exist:[22,25],extend:23,featur:0,fill:26,fillna:26,filter:[14,21],from:[20,23],has_al:25,has_ani:25,has_count:25,horizont:21,how:[36,38],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:34,link:28,list:25,load:12,logic:25,manag:21,map:19,mapper:26,mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:39,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[33,35],numer:26,object:[19,22,23],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,process:18,properti:14,python:0,quantifi:10,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,releas:[33,35],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:34,set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:36,special:13,spss:20,stack:[7,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:34,use:38,valu:[19,22,23,37],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[8,17,31],viewmapp:9,wai:37,weight:14,what:[17,28,38]}}) \ No newline at end of file diff --git a/docs/API/_build/html/sites/api_ref/00overview.html b/docs/API/_build/html/sites/api_ref/00overview.html index 7efd51ebd..fd749a574 100644 --- a/docs/API/_build/html/sites/api_ref/00overview.html +++ b/docs/API/_build/html/sites/api_ref/00overview.html @@ -300,6 +300,7 @@

    Quick search

  • quantify.engine
  • QuantipyViews
  • Rim
  • +
  • Stack
  • View
  • ViewMapper
  • @@ -358,6 +359,7 @@

    API referencesquantify.engine
  • QuantipyViews
  • Rim
  • +
  • Stack
  • View
  • ViewMapper
  • diff --git a/docs/API/_build/html/sites/api_ref/stack.html b/docs/API/_build/html/sites/api_ref/stack.html index 4cfb52a5e..75f4b3018 100644 --- a/docs/API/_build/html/sites/api_ref/stack.html +++ b/docs/API/_build/html/sites/api_ref/stack.html @@ -35,7 +35,10 @@ - + + + + @@ -87,7 +90,7 @@

    Quick search

    - - +
    • meta_to_json() (quantipy.DataSet method) +
    • +
    • min_value_count() (quantipy.DataSet method)
    • missing() (quantipy.View method)
    • @@ -698,6 +706,8 @@

      M

      N

      @@ -756,11 +766,13 @@

      R

    • recode() (quantipy.DataSet method)
    • -
    • reduce() (quantipy.Stack method) +
    • recode_from_net_def() (quantipy.Stack static method) +
    • +
    • reduce() (quantipy.Stack method)
    • -
    • refresh() (quantipy.Stack method) +
    • refresh() (quantipy.Stack method)
    • -
    • remove_data() (quantipy.Stack method) +
    • remove_data() (quantipy.Stack method)
    • remove_html() (quantipy.DataSet method)
    • @@ -789,10 +801,12 @@

      R

    • rescale() (quantipy.Quantity method)
    • rescaling() (quantipy.View method) +
    • +
    • resolve_name() (quantipy.DataSet method)
    • restore_item_texts() (quantipy.DataSet method)
    • -
    • restore_meta() (quantipy.Stack method) +
    • restore_meta() (quantipy.Stack method)
    • revert() (quantipy.DataSet method)
    • @@ -815,7 +829,7 @@

      S

    • (quantipy.DataSet method)
    • -
    • (quantipy.Stack method) +
    • (quantipy.Stack method)
    • select_text_keys() (quantipy.DataSet method) @@ -856,7 +870,7 @@

      S

    • split() (quantipy.DataSet method)
    • -
    • Stack (class in quantipy) +
    • Stack (class in quantipy)
    • start_meta() (quantipy.DataSet static method)
    • @@ -896,6 +910,8 @@

      T

      U

      - - @@ -423,7 +430,7 @@

      Cluster

      - @@ -489,7 +496,7 @@

      Cluster - + diff --git a/docs/API/_build/html/sites/api_ref/DataSet.html b/docs/API/_build/html/sites/api_ref/DataSet.html new file mode 100644 index 000000000..d1a965a12 --- /dev/null +++ b/docs/API/_build/html/sites/api_ref/DataSet.html @@ -0,0 +1,3402 @@ + + + + + + + + + + + DataSet — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      + + + + + + +
      + +
      +
      +
      +
      + +
      +
      +
      +

      DataSet

      +
      +
      +class quantipy.DataSet(name, dimensions_comp=True)
      +

      A set of casedata (required) and meta data (optional).

      +

      DESC.

      +
      +
      +add_meta(name, qtype, label, categories=None, items=None, text_key=None, replace=True)
      +

      Create and insert a well-formed meta object into the existing meta document.

      +

        -
      • variable_types() (quantipy.Stack method) +
      • variable_types() (quantipy.Stack method)
      • variables() (quantipy.DataSet method)
      • @@ -1008,7 +1024,7 @@

        W

        - + diff --git a/docs/API/_build/html/index.html b/docs/API/_build/html/index.html index c7e6b3eeb..6c21b7048 100644 --- a/docs/API/_build/html/index.html +++ b/docs/API/_build/html/index.html @@ -90,9 +90,10 @@

        Quick search

        • Release notes
            -
          • Upcoming (May)
          • -
          • Latest (04/04/2018)
          • +
          • Upcoming (September)
          • +
          • Latest (04/06/2018)
          • Archived release notes
              +
            • sd (04/04/2018)
            • sd (27/02/2018)
            • sd (12/01/2018)
            • sd (18/12/2017)
            • @@ -201,7 +202,7 @@

              Quick search

            • Complex logic @@ -437,7 +439,7 @@

              Key features - + diff --git a/docs/API/_build/html/objects.inv b/docs/API/_build/html/objects.inv index 0c50a0ce953dcd158128843bc2eb58d532806ebd..4b6e8180792853d538e1be1f3a33fe3128febec9 100644 GIT binary patch delta 2310 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
            • Release notes
                -
              • Upcoming (May)
              • -
              • Latest (04/04/2018)
              • +
              • Upcoming (September)
              • +
              • Latest (04/06/2018)
              • Archived release notes
                  +
                • sd (04/04/2018)
                • sd (27/02/2018)
                • sd (12/01/2018)
                • sd (18/12/2017)
                • @@ -197,7 +198,7 @@
                • Complex logic @@ -403,7 +405,7 @@

                  Search

                  - + diff --git a/docs/API/_build/html/searchindex.js b/docs/API/_build/html/searchindex.js index 58f814f8d..ee2a67ab7 100644 --- a/docs/API/_build/html/searchindex.js +++ b/docs/API/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/api_ref/stack","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/lib_doc/overview","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:50,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\api_ref\\stack.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\lib_doc\\overview.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[9,2,1,""],count:[9,2,1,""],exclude:[9,2,1,""],filter:[9,2,1,""],group:[9,2,1,""],limit:[9,2,1,""],normalize:[9,2,1,""],rebase:[9,2,1,""],rescale:[9,2,1,""],summarize:[9,2,1,""],swap:[9,2,1,""],unweight:[9,2,1,""],weight:[9,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[10,2,1,""],add_link:[10,2,1,""],add_nets:[10,2,1,""],add_stats:[10,2,1,""],add_tests:[10,2,1,""],aggregate:[10,2,1,""],apply_meta_edits:[10,2,1,""],cumulative_sum:[10,2,1,""],describe:[10,2,1,""],freeze_master_meta:[10,2,1,""],from_sav:[10,3,1,""],load:[10,3,1,""],reduce:[10,2,1,""],refresh:[10,2,1,""],remove_data:[10,2,1,""],restore_meta:[10,2,1,""],save:[10,2,1,""],variable_types:[10,2,1,""]},"quantipy.Test":{get_se:[9,2,1,""],get_sig:[9,2,1,""],get_statistic:[9,2,1,""],run:[9,2,1,""],set_params:[9,2,1,""]},"quantipy.View":{get_edit_params:[7,2,1,""],get_std_params:[7,2,1,""],has_other_source:[7,2,1,""],is_base:[7,2,1,""],is_counts:[7,2,1,""],is_cumulative:[7,2,1,""],is_meanstest:[7,2,1,""],is_net:[7,2,1,""],is_pct:[7,2,1,""],is_propstest:[7,2,1,""],is_stat:[7,2,1,""],is_sum:[7,2,1,""],is_weighted:[7,2,1,""],meta:[7,2,1,""],missing:[7,2,1,""],nests:[7,2,1,""],notation:[7,2,1,""],rescaling:[7,2,1,""],spec_condition:[7,2,1,""],weights:[7,2,1,""]},"quantipy.ViewMapper":{add_method:[8,2,1,""],make_template:[8,2,1,""],subset:[8,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[9,1,1,""],Rim:[6,1,1,""],Stack:[10,1,1,""],Test:[9,1,1,""],View:[7,1,1,""],ViewMapper:[8,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":39,"boolean":[4,6,10,18,34,35],"break":[],"case":[0,4,5,6,9,10,12,13,15,18,20,23,24,26,28,34,35,37,38],"class":[2,3,4,5,6,7,8,9,10,11,22,34,35],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,34,35,38,39],"export":[0,3,4,20,35],"final":26,"float":[4,5,9,10,19,20,21,22,23,24,31,35],"function":[4,10,20,25,26,28,31,34,35],"import":[4,9,11,15,20,22,25,26],"int":[4,5,9,10,19,20,21,22,23,24,25,34,35],"long":[25,35],"m\u00fcller":0,"new":[3,4,5,8,9,10,12,21,23,24,26,31,34,35,38,39],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,34,35],"short":[8,19,21,24,35],"sigur\u00f0sson":0,"static":[2,3,4,10],"switch":[24,38],"true":[4,5,6,8,9,10,13,20,21,22,24,25,26,31,34,35,38,39],"try":[4,10,23,35],"var":[4,9,15,35,38],"while":[4,15,19,23,24,34,35],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31],IDs:[],NPS:31,Not:[19,24,31],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,34,35,36,37,38],Their:5,Then:[21,35],There:[13,19,21,22,31,35],These:[15,28,31,35],Use:[4,9,24,35],Uses:4,Using:[10,18,21,23],Will:[4,5,9,10,35],With:[13,22,23,24,26,31],Yes:[20,35],__init__:[12,35],__setitem__:35,_band:4,_batch:[10,31],_cumsum:[31,35],_data:[4,11,21],_dimensions_suffix:35,_get_chain:35,_grid:[35,38],_intersect:[28,31],_meta:[4,11,12,13,22,28,39],_missingfi:9,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:35,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,34,35],about:[13,19,22,36],abov:[24,31,39],absorb:35,accept:[23,26,35],access:[4,10,19,20,22,35,36],accessor:22,accid:[21,35],accommod:14,accompani:[19,35],accord:[4,9,10,26,31],accordingli:26,account:[4,5,34],achiev:21,across:[9,22,35],act:[5,24,28],activ:[19,20,24,35],add:[3,4,5,8,10,13,14,23,24,28,31,35,37],add_batch:[12,31],add_chain:3,add_data:10,add_filt:[14,35],add_group:6,add_i:13,add_link:[10,28,31],add_meta:[4,23,34,35,37,39],add_method:8,add_net:[10,31,35],add_open_end:[13,35],add_stat:[10,31,34,35],add_test:[10,31,35],add_tot:35,add_x:[13,31],add_y_on_i:[13,28,31,35],adddit:20,added:[4,10,12,13,14,23,24,28,31,34,35,38],adding:[4,14,18,20,31,35,39],addit:[4,13,19,22,31,34,35],addition:[19,35],adjust:35,adopt:13,aerob:24,affix:10,after:[4,9,10,21,26,28,31,35],afternoon:22,again:[22,23,31],against:[5,9,13,35],agaist:4,age:[4,13,14,22,24,26,28,31,35],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,35],agg:[7,13,31],aggnam:7,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,34,35],aim:[0,35],alasdair:0,alexand:0,algorithm:[4,5,9,35],alia:[4,21],align:35,all:[2,3,4,5,9,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,34,35,37,38],allign:35,allow:[4,14,15,23,24,31,35],alon:[22,35],along:[2,9],alongsid:[10,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,34,35],also:[4,9,10,11,12,13,14,22,23,24,26,28,31,34,35,37,38],altern:[4,23,31,37],although:22,alwai:[4,15,23,24,26,31,35,39],amount:[14,21,28,31],analysi:[0,35],ani:[4,5,7,9,10,13,19,21,22,23,25,26,34,35],anim:[4,19],anoth:[10,13,23,24,26,35],answer:[4,9,10,19,22,23,24,35],anymor:35,anyth:26,anywher:10,api:4,appear:[4,35,37],append:[4,5,18,31,38],appli:[4,5,8,9,10,15,20,21,22,24,28,31,34,35,39],applic:[4,24,26],apply_edit:4,apply_meta_edit:10,apporach:21,approach:37,appropri:[4,5],arab:35,arbitrari:10,arbitrarili:[4,21,25,26],archiv:33,argument:[5,8,11,20,23,24,34,35],aris:28,arithmet:9,around:[4,35],arrai:[4,9,10,11,18,21,22,23,31,34,35,36,37],array_item:35,array_var:39,array_var_1:39,array_var_2:39,array_var_3:39,array_var_grid:39,arriv:9,as_addit:[13,35],as_delimited_set:35,as_df:9,as_float:35,as_int:35,as_singl:35,as_str:35,as_typ:[4,35],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,9],aspect:35,assess:[0,38],assign:[4,6,35,37],assignd:24,associ:[2,10],assum:[4,9,28,38],attach:[4,10,21,23,35],attempt:[4,34],attribut:[2,4,11,22,28,31,35],audit:35,auto:4,autom:[0,10],automat:[4,13,14,20,22,23,24,31,34,35,39],auxiliari:9,avail:4,avoid:26,axes:4,axi:[2,3,4,5,7,9,10,15,19,22,35],axis_edit:[4,35],b_d:4,b_name:31,back:[4,5,38],badli:[4,35],band:[4,10,18,34,35],band_numer:35,bank:3,bank_chain:3,bar:35,base:[4,5,7,8,9,10,18,20,22,31,35],base_al:9,base_text:15,based_on:[4,35],baselin:8,basi:26,basic:[20,22,25,27,35],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,10,15,28,31,34,35],batch_nam:[4,10],batchnam:[11,13],bchain:3,becaus:[24,26,34],becom:[4,9,26,35],been:[4,9,10,13,21,24,26,34,35,39],beer:38,befor:[4,9,10,13,14,24,26,31,34,35],begin:[4,26],behaviour:[5,7,22,26,35,39],being:[4,24,39],belong:[4,11,12,13,31,35,38],below:[9,13,26,35],benefici:21,benefit:26,better:28,between:[4,9,13,31,35],bia:9,big:21,binari:35,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,bla:[],blacklist:[4,35],blank:37,blueprint:35,board:[19,24,39],bool:[4,5,8,9,10,22,38],both:[4,10,12,13,19,20,21,23,24,26,28,34,35,39],bottom3:10,bottom:35,bound:35,bracket:[10,34],brand:38,break_bi:13,breakfast:22,brief:35,broader:19,buchhamm:0,bug:35,bugfix:[34,35],build:[0,3,4,5,9,15,18,19,35],built:[14,31],bunch:34,by_nam:[4,34],by_typ:[4,21,22,35],bytestr:2,cach:[4,10],calc:[5,7,9,10,31,35],calc_onli:[5,31],calcul:[5,9,10,14,27,35],call:[4,10,21,22,24,28,34,35,39],came:35,can:[2,4,5,9,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,34,35,37,38,39],cannot:[26,34,35],cap:6,carefulli:4,carri:5,case1:38,case2:38,case3:38,casedata:[4,20],cast:34,cat:[4,19,23,34,37,39],cat_nam:[10,35],categor:[4,5,10,18,22,23,27,34,35,36],categori:[4,5,10,19,22,23,34,35,37],categorized_nam:[4,35],caught:35,caus:35,caution:4,cave:[19,24,39],cbase:[10,31,35],cbase_gross:35,cell:[5,9,11,26,35],cell_item:14,cellitem:35,central:5,certain:[4,22,34],chain:[0,1,3,16,22,35],chainnam:2,chang:[4,6,9,10,18,21,24,35],charact:[4,23,25],characterist:[19,24],chart:[0,35],check:[4,5,9,10,22,26,34,35],check_dup:35,checking_clust:10,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,10,21,24,35],clean_text:[23,35],clean_up:[4,35],clear:35,clear_factor:4,clone:[4,18],close:12,cluster:[0,1,2,10,13,14,15,35],code:[4,5,6,7,9,10,18,19,22,23,24,31,34,35,37,39],code_count:[4,22,25,35],code_from_label:[4,35],code_map:4,codes_from_nam:[4,35],codes_in_data:[4,22,35],cola:38,collect:[4,5,10,11,13,14,19,21,22,24,27,31,34,35],collect_cod:[10,35],colour:35,coltest:[5,8,10],column:[2,4,5,6,7,9,10,18,20,22,23,24,26,31,35,38,39],combin:[4,5,10,16,22,25,26,28,31,35],combind:38,come:35,comma:25,common:[7,19,24,35],commun:10,compabl:4,compar:[4,5,9,35],comparison:9,compat:[4,10,20,34,35,36],compatibilti:39,complet:[4,9,34,35],complex:[4,5,6,18,19,21,22,26,31,35],compli:[4,26],complic:18,compon:[4,5,7,10,11,15,18,21,22,23,24,35],compos:10,compound:3,comprehens:26,compress:10,comput:[0,4,5,8,9,10,27,28,31,35],concat:[2,22,35],concaten:[2,26],concern:31,cond_map:4,condit:[4,7,9,18,21,22,25,34,35],confirm:10,conflict:34,conjunct:35,connect:[4,10,12,13,35,39],consequ:[28,37],consid:[4,5,9,10,24,34,35],consist:[4,9,11,19,21,23,24,35],constitut:13,constrcut:10,construct:[4,11,13,14,19,21,23,28,31,35,39],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,35,38],content:[21,28],context:[19,23],continu:[],contrast:[19,24],contributor:0,control:[4,5,7,9,10,23,24,26,35],convcrit:6,convent:[4,9,35,39],convers:[4,18,35],convert:[0,4,9,20,24,35,39],coordin:8,cope:28,copi:[2,4,8,12,18,21,23,26,35,39],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,35],copy_not:[4,35],copy_of_batch1:12,copy_onli:[4,35],copy_to:[4,23,35],core:[5,20,22,25,34,35,37],correct:[9,23,35],correctli:[34,35],correspond:[4,5,9,22,23,24,35,37,39],correspons:35,corrupt:34,could:[23,24,26,35],count:[4,5,7,9,10,31,35,38],count_not:[4,22,35],count_onli:[4,22,35],counterpart:35,counts_cumsum:[31,35],counts_sum:31,cpickl:2,crash:35,creat:[2,3,4,6,9,10,11,13,14,15,16,18,20,22,24,28,31,34,35,36],create_set:[4,35],creation:[9,18,23,35],cross:[13,28,31,35],crossbreak:28,crosstab:[4,24],crunch:35,csv:[0,4,10,11,18],cum_sum:9,cumul:[7,9,10,27,35],cumulative_sum:[10,31],current:[0,2,4,10,24,34,35],custom:[4,10,16,18,20,24,35],custom_text:[10,31],customis:35,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:9,cycl:[4,8],dai:[22,26],danish:19,data:[2,4,5,6,7,9,10,11,14,15,20,21,23,24,28,31,34,35,36,38],data_df:[4,35],data_kei:[4,10,14,28,35],datafil:[4,35],datafram:[3,4,6,7,9,10,18,19,21,22,23,28,31,34,35],datakei:35,dataset:[0,1,10,11,12,13,18,20,22,24,28,31,34,35,36,37],dataset_left:4,dataset_right:4,datasmoothi:0,date:[4,10,19,20,21,22,23,24,35],date_format:4,dates_as_str:20,ddf:[0,4,35],deafult:[5,9],deal:20,decim:4,deciph:[0,18],deck:0,decod:10,decode_str:10,decor:35,deep:4,deepcopi:35,defin:[2,3,4,9,10,13,14,19,21,23,28,31,35,37],definit:[2,4,5,9,10,14,19,24,26,27,28,34,35],definiton:[8,19,35],defint:[0,2,4,5,6,9,10,14,19,35],defintit:[21,39],defintiton:23,del:35,deleg:26,delet:[4,10,35],delimied_set:22,delimit:[4,10,19,20,21,22,23,24,25,26,35,37],delimited_set:[22,24,34,35],demograph:21,depend:[5,13,14,35],deprec:[10,35],deriv:[4,9,18,21,34,35,37],derivc:8,derive_categor:35,derot:[4,35,36],desc:4,descend:4,describ:[0,2,3,4,10,14,19,21,28,31,35],descript:[4,5,8,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,35,37],detail:[7,18,19],detect:[4,6],determin:[4,5,10],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,35],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,35,38],dictat:21,dictionari:[4,6,7,8,10,35],differ:[5,7,9,10,13,19,20,23,24,28,31,34,35,36,38],digest:19,dim:[5,9,31],dim_comp:35,dimens:[0,4,5,9,18,35,36],dimension:[4,35],dimensionizing_mapp:4,dimensions_comp:[4,20,35,39],dimlabel:35,dinner:22,direct:26,directli:[2,4,22,35],discret:[10,13],disk:[4,21,35],dispers:5,distinct:[19,31],distribut:[5,9],div:[10,35],dive:[19,24,39],divid:9,dk_filter:4,dms:4,dmsrun:4,dmy:4,doc:[22,35],docstr:6,document:[0,4,10,19,20,23,35],doe:[4,11,23,26,35],doesn:[4,25,26],dog:19,don:[19,22,24,37],done:[4,13,14],doubl:22,down:39,downbreak:[28,31,35],download:39,draft:35,draw:[21,24],drawn:[4,35],drink:38,drink_1:38,drink_2:38,drink_level:38,driven:0,drop:[4,5,9,10,21,34,35],drop_cod:[10,35],drop_delimit:4,drope:[4,38],dropna:[4,6,35,38],dropx:4,dto:39,dtype:[20,22,26,35],due:34,dump:20,duplic:[4,23,34,35],durat:22,dure:[4,9,10,24],each:[2,4,5,6,10,13,14,19,23,24,28,31,35,38,39],eagleston:0,eas:35,easi:[0,19,22,23,35],easier:[12,22,28],easiest:22,easili:[4,12,20,31,35],ebas:[10,35],echo:4,ect:35,edit:[0,4,7,9,13,14,15,18,19,21,24,26,35],edit_param:7,eff:9,effect:[6,9,26,35],effici:34,ein:[19,23],either:[4,5,9,13,19,21,22,23,26,34,35],element:[4,9,10,19,35,39],eleph:4,els:26,emploi:19,empti:[4,10,22,24,26,34,35],empty_item:[4,34],en_u:10,enabl:[23,34,35],encod:[4,35],encount:20,end:[2,4,10,11,12,34,35],end_tim:22,enforc:35,eng:35,engin:[1,27],english:[19,35],enhanc:34,enough:4,enrich:35,ensur:[4,11,34,35],enter:[4,14],entir:[4,9,20,35],entri:[4,6,9,10,19,26,35],enumer:[4,10,23,24,26,37],environ:4,eponym:35,eqaul:4,equal:[9,35],equip:31,equival:[4,39],eras:[4,10],error:[4,9,35],escap:4,especi:[22,26,31],estim:9,etc:[4,19,21,28,31,35],ethnic:[13,22,28,31],even:[4,31,35],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,35],exactli:[4,24,25],exampl:[2,4,5,10,11,18,19,20,22,24,25,28,31,34,35,38,39],excel:[0,3,4,5,35],except:[4,21,34,35],exchang:[9,23],exclud:[4,5,7,9,10,31,35],exclus:[4,25,35],execut:4,exercis:[22,24],exist:[4,9,10,12,13,18,21,23,24,26,31,34,35],expand:[4,7,9,10,31],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,10,35,37],explor:19,expos:23,express:[4,5,6,9,10,22,25,34,35],ext_item:[4,35],ext_valu:4,ext_xk:35,ext_yk:35,extend:[4,5,10,13,18,22,31,35],extend_cod:[10,35],extend_filt:14,extend_i:[13,35],extend_item:[4,35],extend_valid_tk:35,extend_valu:[4,23,35,37],extend_x:35,extens:[3,4,10,19,20,35],extra:35,extract:14,extrapol:25,factor:[4,6,10,34,35],factor_l:[],factor_label:[10,34],factormap:4,fail:[4,35],failur:20,fall:[4,5,9],fallback:7,fals:[4,5,9,10,13,20,22,23,24,26,31,34,35],fast_stack_filt:34,favour:9,featur:[19,35],feed:[4,35],feedback:4,femal:[4,22],few:[11,21,22,35,38],figur:9,file:[2,3,4,5,10,11,20,21,22,28,35,39],file_nam:20,filenam:[2,10],fill:[4,18,35],fillna:[4,18],filter1:35,filter2:35,filter:[4,6,9,10,11,15,18,28,31,34,35],filter_1:35,filter_2:35,filter_def:6,filter_kei:[10,14],filter_nam:35,find:[4,11,22,28,34,35],find_duplicate_text:4,finish:[4,21,35],finnish:19,first:[4,5,9,13,19,21,26,31,35],fit:[19,23,24],fix:[4,15,34,35],flag:[4,5,35],flag_bas:9,flat:4,flatten:[4,24,35],flexibl:[6,10,24],float64:20,folder:20,follow:[4,7,9,11,14,15,19,20,21,22,23,24,26,28,31,35,38,39],folow:26,footbal:24,forc:[26,35],force_text:[4,23,35],form:[3,4,9,10,14,20,23,24,25,26,28,35],format:[0,4,6,9,19,20,23,26,31,35,38],former:[4,10,22,23,24,34],fortnight:22,found:[2,3,4,9,10,20,22,24,26,31,34,35],four:[28,31],frang:[4,25,26,35,38],freez:10,freeze_master_meta:10,french:19,frequenc:[4,5,7,8,9,28,31,35],freysson:0,from:[0,2,3,4,5,9,10,12,14,15,18,19,21,24,25,26,28,31,34,35,38],from_batch:[4,34,35],from_compon:[4,20,35],from_dichotom:[4,35],from_excel:[4,35],from_sav:10,from_set:[4,20,35],from_stack:[4,35],front:35,fulfil:4,full:[3,4,5,10,21,23,25,35,39],fullnam:31,fully_hidden_arrai:4,fun:35,further:21,futur:[10,35],geir:0,gender:[4,13,14,15,22,24,26,28,31,34,35,38],gener:[2,4,5,7,8,10,11,20,22,23,26,31,35],generate_report:35,german:[19,23],get:[4,7,10,11,12,13,14,22,28,31,35,37],get_batch:[4,12,13,28,31],get_edit_param:7,get_properti:4,get_qp_dataset:35,get_s:9,get_sig:9,get_statist:9,get_std_param:7,getter:34,give:[13,26,35],given:[4,5,6,9,10,20,26,35],global:[4,7,9,14,15,21,35],goe:4,going:37,greater:4,grid:[4,35],griffith:0,group:[4,5,6,7,9,10,19,21,22,24,25,28,31,35],group_nam:6,group_target:6,grouped_ag:24,grp:9,grp_text_map:31,guid:35,gzip:10,hack:4,had:26,hand:24,handl:[0,6,8,9,10,20,31,35,37,39],handler:34,happen:[4,24],happend:4,has:[2,4,10,12,15,19,21,25,26,31,34,35,38],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:7,have:[4,9,10,13,20,23,24,25,26,28,34,35,38],head:[20,22,23,24,26,35,39],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,10,26,28,34,35,39],hidden:[4,35],hide:[4,15,34,35],hide_empti:34,hide_empty_item:[4,34],hide_valu:[4,35],high:[5,9],higher:9,hmerg:[4,35],hockei:24,hold:[2,4,9,10],horizont:[4,18],household:20,how:[3,4,10,14,19,22,24,28,31,37,39],howev:[23,24,26,37,39],hrafn:0,html:[4,23,35],ident:[4,20,24,34,35],identif:4,identifi:[4,5,23,35,38],ids:4,ignor:[4,9,10,22,24,35],ignore_arrai:4,ignore_cod:4,ignore_flag:9,ignore_item:[4,24,35],ignore_valu:[4,24],ill:23,implement:[9,10,19,22,35],impli:4,implicitli:9,impract:28,impute_method:6,incl:[10,35],includ:[2,3,4,5,9,10,13,25,26,28,31,34,35,38],inclus:[24,35],incom:[4,9,24],inconsist:[4,15,23,35],incorrect:35,incorrectli:[34,35],independ:9,index:[0,2,4,9,10,35],indic:[4,5,9,10,23,24,26,35],individu:[3,4,35],industri:20,infer:[10,20,24,35],info:[4,18,19],inform:[0,4,7,8,13,14,15,19,20,23,26,31,35,37,38],inherit:[11,35],inhomogen:9,init:26,initi:[4,18,35,38],inject:[4,26],innermost:7,inplac:[4,9,10,18,21,26,35],input:[4,6,9,10,11,20,35],insert:[4,13,34],insid:[2,4,8,9,19,20,22,23,34,35],inspect:[4,18,35],instal:[4,35],instanc:[2,3,4,8,9,10,11,14,20,21,22,23,24,28,31,34,35],instead:[4,9,10,20,25,26,34,35],instruct:[5,9,19,26,31,35],int64:[20,22],integ:9,integr:22,intend:10,inter:[4,6],interact:[2,3,4,21,22,35],interfac:0,interim:35,interlock:[4,18,35],intern:[2,34,35],interpret:[4,25],intersect:[4,18,22,35],intro:21,introduc:9,involv:26,iolocal:[10,20],ioutf8:10,ipython:[4,21,35],is_arrai:[31,35],is_bas:7,is_block:31,is_count:7,is_cumul:7,is_dat:35,is_delimited_set:35,is_float:35,is_g:[4,25,35],is_int:35,is_like_numer:[4,22,24,35],is_meanstest:7,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:7,is_pct:7,is_propstest:7,is_singl:35,is_stat:7,is_str:35,is_sum:7,is_weight:[7,31],isol:35,issu:[20,35],ist:[19,23],item:[4,9,11,13,19,20,21,22,23,24,31,34,35,37,38,39],item_nam:[4,35],item_no:[4,35],item_text:[4,22,35],iter:[8,22,35],its:[2,4,5,7,9,10,11,12,13,14,15,19,20,22,24,25,26,31,35,38,39],itself:[4,9],jame:0,jjda:20,jog:24,join:[4,22],json:[4,10,11,18,35],jupyt:[4,21,35],just:34,keep:[4,8,9,21,23,24,35,38],keep_bas:9,keep_cod:[9,35],keep_origin:4,keep_variable_text:[4,35],kei:[2,4,5,7,10,11,14,19,23,24,26,31,35,38],kept:[4,9,11,35],kerstin:0,keyword:[5,8,35],kind:[22,24,28,35],kite:[19,24,39],know:[14,19,22,24,37,39],known_method:[],kritik:31,kwarg:[4,5,7,8,35],lab:10,label:[4,5,10,14,19,20,22,23,24,26,34,35,37,38,39],lack:0,lang:19,languag:[11,18,23,35],larg:[14,21,28,31,35],last:4,later:[4,13,20,35],latest:[0,33,35],latter:[22,34,35],lead:[24,31,34],least:[4,22,23],leav:[23,24],left:[4,9,35],left_id:4,left_on:[4,35],legaci:4,lemonad:38,length:[4,35],less:[22,38],let:[23,24,28],letter:[],level:[4,5,9,10,14,19,26,31,35,38],lib:[4,19,35,39],librari:[0,19,35],lift:24,like:[0,4,5,10,13,14,19,21,22,24,26,28,31,34,35,38,39],limit:[4,9,24,26,35],line:[],link:[2,4,5,7,8,9,10,11,14,27,31,35],list:[2,4,5,6,8,9,10,13,18,19,21,22,23,24,26,34,35,37,38],list_vari:35,listen:26,load:[2,3,4,10,11,35],load_cach:10,loc:31,local:[10,13,22,28,31],locat:[2,4,10,35],logic1:35,logic2:35,logic:[4,5,7,9,18,22,24,26,34,35],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:35,look:[13,24,26,35,38,39],loop:[35,38],lose:[13,24,38],lot:[4,21,22,26],low:[5,9],lower:[4,9,34,35],lower_q:9,lunch:22,machin:[4,20,35],made:2,mai:[4,25,26,33],main:[4,6,9,13,14,19,24,31,35],main_filt:35,mainli:3,major:9,mak:35,make:[4,5,22,24,28,35],make_summari:[13,35],make_templ:8,male:[22,26],manag:[0,18,26],mani:[13,28],manipul:[9,13,15],manual:[5,35,37],map:[4,5,6,8,9,10,18,20,23,34,35,37],mape:4,mapper:[4,18,25,35,38],mapper_to_meta:4,march:[],margin:[5,9],mark:9,market:[0,20],mask:[4,18,21,22,23,34,35,37,38,39],mass:5,massiv:26,master:35,master_meta:10,match:[4,6,10,20,24,35,39],matric:9,matrix:9,matrixcach:10,matter:11,max:[9,10,31,35],max_iter:6,mdd:[0,4,20,35],mdm:[],mdm_lang:35,mean:[4,5,6,7,9,10,13,15,20,22,24,25,26,31,35,38],measur:[5,9],median:[6,9,10,31,35],membership:[22,35],memori:[21,35],memoryerror:35,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,35],merge_text:[4,35],messag:35,meta:[4,5,7,10,11,12,14,15,18,20,21,22,24,26,31,35,37,38,39],meta_dict:[4,35],meta_edit:[4,10,15],meta_to_json:[4,35],metadata:[0,4,10,18,19,20,24,26,34,35,37,39],metaobject:35,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,34,35,37,38,39],metric:[5,9,10],mid:[5,9,22,35],middl:35,might:[10,21,23,24,34],mimic:[5,9],mimick:[5,9,22],min:[9,10,31,35],minimum:5,minor:35,mismatch:35,miss:[4,6,7,14,20,22,23,24,37,39],missing_map:[4,35],mix:[4,24],mode:[6,10,20,35,36],modifi:[4,9,10,15,24,35],modu:35,modul:[0,10],month:22,more:[2,4,22,25,26,31,34,35],morn:22,most:[9,21,22,26],mous:4,move:[0,4,21,35],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,38],mul:10,multi:[5,9,19],multiindex:9,multipl:[0,3,4,5,19,22,23,25,26,35],multipli:9,multivari:9,must:[4,6,10,20,21,23,26,31,35,38],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,34,35,37,38,39],name_data:[35,39],nan:[4,9,20,22,23,24,26,28,31,34,35,39],nate:4,nativ:[0,4,18,35],natur:[9,21,22],necessari:[9,21,35,38],need:[4,8,9,10,21,23,24,35,39],neg:35,nest:[7,10,19,25,26],net1:[],net2:[],net:[5,7,9,10,18,27,34,35],net_1:[10,31],net_2:[10,31],net_3:10,net_map:[10,31],net_view:35,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:35,new_cod:9,new_column:4,new_d:35,new_data:10,new_data_kei:10,new_dataset:4,new_int:23,new_meta:10,new_nam:[4,23,24,35],new_ord:[4,21,35],new_rul:35,new_set:[4,35],new_singl:23,new_stack:2,new_text:[4,23],new_var:35,new_weight:10,next:[4,10,11,19,31,34,39],no_data:24,no_filt:[4,10,28,31,35],non:[4,10,22,25,27,35],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,34,35,37,39],none_band:35,nonea:4,normal:[4,5,9,10,34],norwegian:19,not_al:18,not_ani:[4,9,18,35],not_count:18,notat:[5,7,9,10],note:[2,4,5,26,31],notebook:[4,21,35],notimplementederror:[15,35],novemb:[],now:[13,14,21,31,34,35,38],num:5,number:[4,6,9,20,22,23,24,35],numer:[4,5,10,18,19,23,24,31,34,35,37],numpi:[0,9,34],obei:22,object:[2,3,4,5,9,10,18,20,21,24,26,28,31,34,35,37],obscur:26,observ:0,obvious:31,occur:26,octob:[],oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,35,37,39],often:[21,22,24],old:[4,5,10,35],old_cod:9,old_nam:35,older:35,omit:26,on_var:[10,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,35],ones:[4,9,15,21,22],onli:[4,8,9,10,11,13,14,21,23,24,26,28,31,34,35,37,38],only_men:24,only_typ:[4,10],onto:26,oom:35,open:[0,4,11,12,35],oper:[4,5,20,22,24,25,26,35],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,9,10,13,14,19,22,23,31,34,35],order:[2,4,11,18,19,24,34,35],ordereddict:[3,4,10,13,28,31],organ:16,orgin:9,orient:[2,35],origi:4,origin:[4,9,10,21,24,35],other:[4,5,9,10,11,12,13,19,23,24,25,26,35,38],other_sourc:[10,31,35],otherwis:[4,34,35],our:[0,22,26],out:[5,12,14,24,31,35],outcom:4,outdat:35,output:[4,9,10,13,22,35],outsid:35,over:[4,8,9,10,35],overcod:[4,10,31],overlap:9,overview:[4,10,22,28,35],overwrit:[4,15,21,26,35],overwrite_margin:9,overwrite_text:4,overwritten:[4,10,35],ovlp_correc:9,own:[13,35],pack:4,packag:5,paint:[3,35],painter:35,pair:[4,5,9,18,23,26,35,37],panda:[0,3,4,6,9,10,19,20,22,26,35],pane:4,parachut:[19,24,39],parallel:10,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,34,35,38],parent:[4,19,20,35],pars:5,part:[4,7,19,21,22,26,35],parti:18,particip:20,particular:4,pass:[4,5,9,10,21,22,24,34,35],past:26,path:[2,3,4,10,20,35],path_clust:3,path_csv:20,path_data:[4,39],path_ddf:[4,20,35],path_json:20,path_mdd:[4,20,35],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:10,path_txt:20,path_xlsx:[4,35],path_xml:20,pct:4,peopl:20,per:[4,5,6,9,10,12,20,22,23,24,26,34,35,39],percentag:[7,9,10,31,35],perform:[4,5,7,9,10,21,26,31,35],perman:21,physic:35,pick:[4,24],pickl:2,pilat:24,pivot:10,place:[4,9,26,35],plai:22,plain:[0,7,20],plan:[11,13,14,19,24,28,31],pleas:[10,21,22,35],point:19,pointer:19,pool:9,popul:[4,11,13,14,27,31,35],portion:10,posit:[4,9,21,22,23,28,35],possibl:[3,4,5,10,12,13,19,22,23,25,31,35,38],power:0,powerpoint:[5,35],powerpointpaint:35,pptx:35,pre:[4,26,31],precis:[26,35],predict:34,prefer:22,prefix:[4,10],prep:25,prepar:[3,21,23,31,35],present:[4,9,35],preset:[4,10],pretti:[4,26],prevent:[4,15,21,23,24,34,35],previou:[21,35],previous:[4,35],primarili:4,print:[13,22,28,31,35],prior:[4,24],prioriti:35,probabl:[19,24,35],problem:35,process:[0,4,8,10,20,21,22],produc:[5,9,13,24],product:[34,38],profession:[4,35],progress:[4,35],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[35,39],properli:39,properti:[4,9,11,20,35],proport:[5,6,7],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,35,37],proxi:10,purpos:19,put:10,python:4,q01_1:4,q01_3:4,q11:35,q11_grid:35,q12:38,q12_10:38,q12_11:38,q12_12:38,q12_13:38,q12_1:38,q12_2:38,q12_3:38,q12_4:38,q12_5:38,q12_6:38,q12_7:38,q12_8:38,q12_9:38,q12_:38,q12a:38,q12a_10:38,q12a_11:38,q12a_12:38,q12a_13:38,q12a_1:38,q12a_2:38,q12a_3:38,q12a_4:38,q12a_5:38,q12a_6:38,q12a_7:38,q12a_8:38,q12a_9:38,q12a_grid:38,q12b:38,q12b_10:38,q12b_11:38,q12b_12:38,q12b_13:38,q12b_1:38,q12b_2:38,q12b_3:38,q12b_4:38,q12b_5:38,q12b_6:38,q12b_7:38,q12b_8:38,q12b_9:38,q12b_grid:38,q12c:38,q12c_10:38,q12c_11:38,q12c_12:38,q12c_13:38,q12c_1:38,q12c_2:38,q12c_3:38,q12c_4:38,q12c_5:38,q12c_6:38,q12c_7:38,q12c_8:38,q12c_9:38,q12c_grid:38,q12d:38,q12d_10:38,q12d_11:38,q12d_12:38,q12d_13:38,q12d_1:38,q12d_2:38,q12d_3:38,q12d_4:38,q12d_5:38,q12d_6:38,q12d_7:38,q12d_8:38,q12d_9:38,q12d_grid:38,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,38],q1_2:[4,26,38],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:35,q4a_1:35,q4a_2:35,q4a_3:35,q4a_grid:35,q5_1:[19,21,22,24,35,39],q5_2:[19,21,22,24,35,39],q5_3:[19,21,22,24,35,39],q5_4:[19,21,22,24,35,39],q5_5:[19,21,22,24,35,39],q5_6:[19,21,22,24,35,39],q5_grid:39,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:39,q6_net:31,q6copi:39,q6new:39,q6new_grid:39,q6new_q6copi:39,q6new_q6copy_grid:39,q6new_q6copy_tran:39,q6new_q6copy_trans_grid:39,q7_1:[21,22,35],q7_2:[21,22,35],q7_3:[21,22,35],q7_4:[21,22,35],q7_5:[21,22,35],q7_6:[21,22,35],q7_grid:39,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:38,q_label:[4,35],qtp:38,qtype:[4,23,37,39],qualifi:[5,9],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,34,35,37,39],quantipyview:[1,10,35],quantiti:[9,35],queri:[2,4,6,10,18,19,24,35,39],question:[4,9,19,24,26,31,35,38],questionnair:21,quick:[4,22,35],quickli:[6,21,22,24,35],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,34,35],rake:35,rang:[4,5,18,21,35],rate:[35,38],raw:[5,9],raw_sum:9,rbase:10,read:[0,4,20,35],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,35,39],read_spss:[4,20],rebas:9,rebuild:24,rec:[4,35],receiv:38,recod:[0,4,10,18,35],recode_seri:4,recoded_filt:35,recommend:24,record_numb:[13,22],reduc:[4,9,10,21,35],reduced_d:21,reduct:4,refactor:35,refer:[4,9,10,19,23,26,28,31,35],referenc:[10,13,19,26,35],reflect:[4,9,21,35],refresh:10,refus:[19,24],regard:[4,35],region:[4,35],regist:[4,22,35],regroup:34,regular:[4,19,31,34],regularli:[22,23,24],reindex:4,reintroduc:35,rel:21,rel_to:7,relat:[2,7,23,26,35],relation_str:7,relationship:10,relev:[23,34],religion:22,reload:[21,35],remain:[4,5,21,26,35],rememb:37,remind:37,remov:[4,5,9,10,12,13,18,34,35],remove_data:10,remove_filt:35,remove_html:[4,35],remove_item:4,remove_valu:4,renam:[4,18,24,35,39],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,35],reorder_item:[4,35],reorder_valu:4,reorgan:0,repair:[4,35],repair_text_edit:[4,35],repeat:[21,28],repetit:26,replac:[4,9,13,23,26,31,35],replace_i:[13,35],replace_text:[4,35],report:[0,4,5,6,34,35],reposit:[4,21,35],repres:[4,26],represent:[7,9,10,19,24],request:[5,10,13,21,23,26,31,35],request_view:35,requir:[4,21,23,35,39],rescal:[5,7,9,10,31],research:[0,20],reserach:[],reset:[4,35],reset_index:4,resid:35,resolv:34,resolve_nam:34,resp:34,respect:[4,9,24,34,35,37],respond:[9,21,38],respons:[19,22,25,26,35,38],responsess:35,restor:[4,10,21,35],restore_item_text:4,restore_meta:10,restrict:[4,5,9,10,19,21,22,34,35],result:[3,4,5,8,9,10,16,20,22,23,24,26,28,35],result_onli:9,retain:8,retriev:9,revers:[24,25],revert:[4,21,35],right:[4,35],right_id:4,right_on:[4,35],rim:[1,4,35],roll:4,roll_up:4,rollback:[18,35],rolled_up:4,round:6,row:[4,5,9,18,20,22,35,39],row_id:4,row_id_nam:4,rule:[4,34,39],run:[4,9,10,15,24,28,31,34,35],safe:[4,23],safeguard:4,sai:26,same:[3,4,10,13,19,20,22,26,28,34,35,38,39],sampl:[5,7,9,35],sample_s:14,sandbox:35,satisfi:35,sav:[4,10,20],save:[2,3,4,10,21,28,35],savepoint:18,scalar:35,scale:[5,6,9,19,34,38],scenario:39,scheme:[4,6,19,35],scratch:[18,35],script:4,search:4,second:[4,5,9,15,31],sect:4,section:[7,9,11,14,21,26],see:[13,21,24,26,28,31,35],seen:[26,39],segemen:26,segment:18,select:[4,8,10,13,14,21,22,31,35,38],select_text_kei:4,self:[2,4,9,10,26,28,35],sem:[9,10],semi:34,sensit:34,separ:[4,26,37],septemb:[],seri:[4,19,22,26,35],serial:2,session:[21,35],set:[3,4,5,6,8,9,10,11,12,13,18,19,20,22,23,24,26,28,34,35,37],set_cell_item:14,set_col_text_edit:35,set_column_text:35,set_dim_suffix:35,set_encod:4,set_factor:4,set_item_text:[4,23,35],set_languag:14,set_mask_text:35,set_miss:[4,35],set_opt:[34,35],set_param:9,set_properti:[4,15],set_sigtest:[14,31,35],set_target:6,set_text_kei:4,set_unwgt_count:35,set_val_text_text:35,set_value_text:[4,15,23,35],set_var:[],set_variable_text:[4,15,23,35],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,35],setup:[4,9,13,34,35],sever:[5,22,38],shape:[4,21,31],share:[4,19],sheet:[4,35],shop:4,short_item_text:35,shorten:[4,35],shorthand:[4,5],shortnam:[5,7],should:[3,4,10,14,21,22,26,35,38,39],show:[4,9,13,19,21,22,31,35],shown:[4,9,35],side:[8,35],sig:[5,9,10,14,35],siglevel:35,signific:[5,9,11,27],significancetest:35,sigproperti:35,similar:[28,38],similarli:[22,23],similiar:23,simpl:[5,6,9,19,25,34,35,37],simpli:[4,22,23,24,25,31,34,35],simplifi:[24,26],sinc:[9,26,31,39],singl:[3,4,10,19,20,21,22,23,24,26,34,35,37,39],sit:26,six:22,size:[5,7,9,21,22,31,35],skip:[22,23,31,35],skip_item:35,slice:[4,7,15,18,35],slicer:[4,18,22,24,35],slicex:4,small:[5,35],snack:22,snapshot:[4,21,35],snowboard:[19,24,39],soccer:24,social:0,softwar:[0,4,5,9,20],solut:35,solv:35,some:[13,14,15,22,25,26,35,38],someth:38,sometim:[21,28,34],soon:35,sorri:0,sort:[4,15,35],sort_by_weight:4,sortx:4,sourc:[0,4,10,19,20,22,23,35,39],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:7,specfic:14,special:[0,11,14,19,28,31,35],specif:[3,4,5,7,9,10,11,13,14,15,19,21,23,34,39],specifi:[2,4,5,6,9,10,13,20,23,31,35,37],speed:35,split:[4,10,13,35],split_view_nam:10,sport:[20,22],spreadsheet:0,spss:[0,4,9,18,35],spss_limit:[4,35],squar:10,stack:[2,3,4,5,7,11,13,14,27,31,34,35],stage:[4,35],standalon:18,standard:[7,9,20],standardli:24,start:[4,18,23,24,26],start_meta:[4,35],start_tim:22,stat:[4,5,9,10,31,35],state:[4,15,18,24,35],statement:[4,5,19,25,26],statisfi:35,statist:[0,4,5,7,9,10,18,27,28,35],std_paramet:7,stddev:[9,10,31,35],ste:35,stem:35,step:[31,37],still:[26,35],store:[4,5,10,11,12,13,19,21,24,28,31,35],store_cach:10,str:[3,4,5,6,7,8,9,10,24,34,35,37,38],str_tag:34,strict:[4,35],strict_select:8,strictli:23,string:[2,4,6,7,9,10,19,20,21,22,23,24,25,34,35],strip:35,structur:[0,4,6,8,10,11,13,19,20,21,24,28,35,38],studi:35,stuff:[],style:[4,10],sub:[10,31],subclass:[2,11],subclasss:15,sublist:34,subset:[4,8,18,22,24,34,35],subset_d:4,substr:34,subtl:35,subtyp:[19,35,39],suffix:[4,5,24,34,35,39],sum:[4,5,7,9,10,27,35],summar:[4,5,9,35],summari:[4,5,6,7,9,10,13,22,31,34,35],summaris:10,summat:9,suppli:24,supporintg:10,support:[0,7,10,18,19,22,23,24,35,39],surf:[19,24,39],survei:21,sv_se:[31,35],swap:[7,9,10,35],swedish:[19,35],swim:24,syntax:35,sys:4,tab:20,tabl:[0,10],tabul:[5,13,28],tag:[4,23,34,35],take:[4,5,10,11,22,24,25,26,34,35,38],taken:[4,10,14,15,24,35],target:[4,6,18,23,35],target_item:4,task:22,team:22,temp:4,templat:[5,8,34,35],temporari:[4,35],temporarili:4,ten:26,tend:4,term:[10,23,35],termin:35,test:[2,4,5,7,9,11,22,27,34,35,37],test_cat_1:37,test_cat_2:37,test_cat_3:37,test_tot:[5,9,35],test_var:[34,37],testtyp:9,text1:20,text:[4,5,7,10,14,18,20,21,22,24,26,31,34,35,37,39],text_kei:[3,4,10,11,18,22,23,31,35],text_label:[4,35],text_prefix:10,textkei:[4,35],than:[4,22,24,35],thei:[4,9,13,14,20,25,26,31,35],them:[4,5,13,20,22,26,31,34,35],themselv:[4,9],therefor:[4,5,24,35],thi:[2,3,4,5,6,9,10,13,14,15,20,21,22,23,24,26,28,31,34,35,38,39],third:18,thorugh:24,those:4,three:[4,21,22,24,26,35,37],threshold:5,through:[2,3,4,8],throughout:[4,19,20,35],thu:6,time:[10,19,21,22],titl:13,tks:35,to_arrai:[4,35,38],to_delimited_set:[4,35],to_df:9,to_excel:10,todo:[4,5,6,8,9,10],togeth:[3,4,10,19,21],toggl:10,too:35,tool:[5,20,24,25],top2:31,top3:10,top:26,topic:[19,35],total:[4,5,6,9,13,35],toward:35,tracker:35,tradit:9,transfer:35,transform:[0,4,5,9,18,34],translat:23,transpos:[4,13,24,31,39],transpose_arrai:13,transposit:24,treat:[4,9,25,31],tree:28,treshhold:9,trigger:4,tstat:9,tupl:[4,7,23,24,35,37],turn:19,two:[4,5,9,13,19,21,23,28,31,35],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,34,35,37,39],type_nam:10,typic:26,ultim:4,unabbrevi:25,unattend:4,uncod:[4,35],uncode_seri:4,uncodit:5,unconditi:9,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:10,uni:[5,9],unicod:10,unifi:[4,35],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,10,24,28,31,35],unique_id:22,unique_kei:[4,35,38],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:9,unqiu:24,unrol:[4,31,35],untouch:23,unusu:35,unwant:[4,35],unweight:[9,10,31,35],unweighted_bas:[10,31,35],unwgt:35,upcom:33,updat:[4,7,8,9,23,34,35],update_axis_def:[9,35],update_exist:[4,35],upon:19,upper:[4,35],upper_q:9,usag:[23,35],use:[0,2,4,5,9,10,12,13,19,20,21,22,23,24,26,34,35,36],use_ebas:9,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,34,35],useful:[21,22,35],user:[2,4,14,34,35,37],userwarn:[35,37],uses:[4,9,34],using:[0,2,3,4,6,10,19,20,21,24,25,26,28,31,34,35,39],usual:19,utf:10,val:4,val_text_sep:4,valid:[4,6,10,14,19,24,26,31,34,35,37],valid_cod:35,valid_tk:[11,35],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,34,35,36,39],value_count:[22,35],value_map:38,value_text:[4,22,35],valueerror:[4,21,23,34,35],var_exist:[22,35],var_grid:38,var_nam:[35,38],varcoeff:9,vari:22,variabl:[0,4,5,6,7,9,10,11,18,19,20,23,27,34,35,37,38,39],variable_typ:10,variables_:[],variables_from_set:35,varianc:9,variant:[22,34],varibal:38,varibl:35,variou:[5,14,22,28,31],varlist:[4,35],varnam:[4,35],vector:9,verbatim:11,verbos:[4,10,25,31,35],veri:[19,23,24,34,35],verify_nam:4,versa:9,version2:24,version:[4,5,9,10,19,21,23,24,26,34,35],versu:31,vertic:[4,18],via:[0,4,5,10,21,22,23,24,31,34,35],vice:9,view:[1,2,3,4,5,8,9,10,14,16,22,27,28,35],view_kei:31,view_name_not:9,viewmanag:35,viewmapp:[1,10],viewmeta:7,visibl:[31,35],vmerg:[4,35],wai:[10,12,13,19,21,22,23,26,31,34,35,36],wait:21,want:[21,24,26],warn:[4,31,34,35],water:38,wave:21,week:22,weight:[0,4,6,7,9,10,11,12,22,24,31,35],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,well:[4,9,20,22,25,26,31,35,39],went:35,were:[26,35],wgt:35,what:[16,19,20,24,26,27,35,36,39],whatev:[4,26],when:[4,5,9,10,20,21,23,24,26,34,35,39],where:[2,3,4,9,24,25,26],whether:[5,9],which:[4,5,9,10,11,13,14,15,22,23,24,26,28,31,35,38],whole:[4,35],whose:[4,10,35],wide:35,wil:4,wildcard:26,window:20,windsurf:[19,24,39],wise:[4,31],witch:35,within:[4,9],without:[34,35],women:15,work:[4,11,21,23,31,35],workbook:3,workspac:34,world:20,would:[4,19,24,26,34],wouldn:[19,24],wrap:35,wrapper:[4,9],write:[4,10,20,21,35,39],write_dimens:[4,35],write_quantipi:[4,21],write_spss:[4,20],writen:38,written:[21,34,35],wrong:35,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:9,xks:[4,10,31,34,35],xlsx:4,xml:[4,20],xsect:9,xtotal:4,y_filter:35,y_kei:[14,28,31],y_on_i:[13,28,31,35],year:[19,22,39],yes:20,yield:22,yks:[4,35],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,35,38,39],younger:24,your:[4,19,20,21,24,26,35],ysect:9},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","View","ViewMapper","quantify.engine","Stack","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Documentation","Release notes","Upcoming (May)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,39],Using:20,about:38,access:39,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:35,arrai:[13,19,24,38,39],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,37],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:39,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,37,39],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,39],datafram:20,dataset:[4,15,19,21,23,38,39],deciph:20,defin:[],definit:31,deriv:26,derot:38,descript:31,detail:26,dice:22,differ:37,dimens:[20,39],document:32,edit:23,end:13,engin:[9,29],exampl:26,exist:[22,25],extend:23,featur:0,file:[],fill:26,fillna:26,filter:[14,21],from:[20,23],full:[],gener:[],has_al:25,has_ani:25,has_count:25,horizont:21,how:[36,38],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:34,link:28,list:25,load:12,logic:25,mai:34,main:[],manag:21,map:19,mapper:26,march:[],mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:39,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[33,35],novemb:[],numer:26,object:[19,22,23],octob:[],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,posit:[],process:18,properti:14,python:0,quantifi:9,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,rel:[],releas:[33,35],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:[],set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:36,special:13,spss:20,stack:[10,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:34,use:38,valu:[19,22,23,37],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[7,17,31],viewmapp:8,wai:37,weight:14,what:[17,28,38]}}) \ No newline at end of file +Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/Stack","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/lib_doc/overview","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:52,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\Stack.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\lib_doc\\overview.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],drop_duplicates:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],min_value_count:[4,2,1,""],names:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],resolve_name:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],unbind:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[10,2,1,""],count:[10,2,1,""],exclude:[10,2,1,""],filter:[10,2,1,""],group:[10,2,1,""],limit:[10,2,1,""],normalize:[10,2,1,""],rebase:[10,2,1,""],rescale:[10,2,1,""],summarize:[10,2,1,""],swap:[10,2,1,""],unweight:[10,2,1,""],weight:[10,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[7,2,1,""],add_link:[7,2,1,""],add_nets:[7,2,1,""],add_stats:[7,2,1,""],add_tests:[7,2,1,""],aggregate:[7,2,1,""],apply_meta_edits:[7,2,1,""],cumulative_sum:[7,2,1,""],describe:[7,2,1,""],freeze_master_meta:[7,2,1,""],from_sav:[7,3,1,""],load:[7,3,1,""],recode_from_net_def:[7,3,1,""],reduce:[7,2,1,""],refresh:[7,2,1,""],remove_data:[7,2,1,""],restore_meta:[7,2,1,""],save:[7,2,1,""],variable_types:[7,2,1,""]},"quantipy.Test":{get_se:[10,2,1,""],get_sig:[10,2,1,""],get_statistic:[10,2,1,""],run:[10,2,1,""],set_params:[10,2,1,""]},"quantipy.View":{get_edit_params:[8,2,1,""],get_std_params:[8,2,1,""],has_other_source:[8,2,1,""],is_base:[8,2,1,""],is_counts:[8,2,1,""],is_cumulative:[8,2,1,""],is_meanstest:[8,2,1,""],is_net:[8,2,1,""],is_pct:[8,2,1,""],is_propstest:[8,2,1,""],is_stat:[8,2,1,""],is_sum:[8,2,1,""],is_weighted:[8,2,1,""],meta:[8,2,1,""],missing:[8,2,1,""],nests:[8,2,1,""],notation:[8,2,1,""],rescaling:[8,2,1,""],spec_condition:[8,2,1,""],weights:[8,2,1,""]},"quantipy.ViewMapper":{add_method:[9,2,1,""],make_template:[9,2,1,""],subset:[9,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[10,1,1,""],Rim:[6,1,1,""],Stack:[7,1,1,""],Test:[10,1,1,""],View:[8,1,1,""],ViewMapper:[9,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":39,"boolean":[4,6,7,18,35],"case":[0,4,5,6,7,10,12,13,15,18,20,23,24,26,28,34,35,37,38],"class":[2,3,4,5,6,7,8,9,10,11,22,34,35],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,34,35,38,39],"export":[0,3,4,20,35],"final":26,"float":[4,5,7,10,19,20,21,22,23,24,31,34,35],"function":[4,7,20,25,26,28,31,34,35],"import":[4,10,11,15,20,22,25,26],"int":[4,5,7,10,19,20,21,22,23,24,25,34,35],"long":[25,34,35],"m\u00fcller":0,"new":[3,4,5,7,9,10,12,21,23,24,26,31,34,35,38,39],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,34,35],"short":[9,19,21,24,35],"sigur\u00f0sson":0,"static":[2,3,4,7],"switch":[24,34,38],"true":[4,5,6,7,9,10,13,20,21,22,24,25,26,31,34,35,38,39],"try":[4,7,23,35],"var":[4,10,15,34,35,38],"while":[4,15,19,23,24,34,35],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31,34],NPS:31,Not:[19,24,31,34],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,34,35,36,37,38],Their:5,Then:[21,35],There:[13,19,21,22,31,35],These:[15,28,31,35],Use:[4,10,24,35],Uses:4,Using:[7,18,21,23],Will:[4,5,7,10,35],With:[13,22,23,24,26,31],Yes:[20,35],__init__:[12,35],__setitem__:35,_band:4,_batch:[7,31],_cumsum:[31,35],_data:[4,11,21],_dimensions_suffix:35,_get_chain:35,_grid:[35,38],_intersect:[28,31],_meta:[4,11,12,13,22,28,39],_missingfi:10,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:35,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,35],about:[13,19,22,36],abov:[24,31,39],absorb:35,accept:[23,26,35],access:[4,7,19,20,22,35,36],accessor:22,accid:[21,35],accommod:14,accompani:[19,35],accord:[4,7,10,26,31],accordingli:26,account:[4,5,34,35],achiev:21,across:[10,22,35],act:[5,24,28],activ:[19,20,24,35],add:[3,4,5,7,9,13,14,23,24,28,31,34,35,37],add_batch:[12,31],add_chain:3,add_data:7,add_filt:[14,35],add_group:6,add_i:13,add_link:[7,28,31],add_meta:[4,23,34,35,37,39],add_method:9,add_net:[7,31,35],add_open_end:[13,34,35],add_stat:[7,31,35],add_test:[7,31,35],add_tot:35,add_x:[13,31],add_y_on_i:[13,28,31,35],adddit:20,added:[4,7,12,13,14,23,24,28,31,34,35,38],adding:[4,14,18,20,31,35,39],addit:[4,13,19,22,31,34,35],addition:[19,34,35],adjust:[34,35],adopt:13,aerob:24,affix:7,after:[4,7,10,21,26,28,31,34,35],afternoon:22,again:[22,23,31],against:[4,5,10,13,35],age:[4,13,14,22,24,26,28,31,35],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,35],agg:[8,13,31],aggnam:8,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,34,35],aim:[0,35],alasdair:0,alert:34,alexand:0,algorithm:[4,5,10,35],alia:[4,21],align:35,all:[2,3,4,5,7,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,34,35,37,38],allign:35,allow:[4,14,15,23,24,31,34,35],alon:[22,35],along:[2,10],alongsid:[7,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,35],also:[4,7,10,11,12,13,14,22,23,24,26,28,31,34,35,37,38],altern:[4,23,31,37],although:22,alwai:[4,15,23,24,26,31,35,39],amount:[4,14,21,28,31],analysi:[0,35],ani:[4,5,7,8,10,13,19,21,22,23,25,26,34,35],anim:[4,19],anoth:[7,13,23,24,26,35],answer:[4,7,10,19,22,23,24,35],anymor:[34,35],anyth:26,anywher:7,api:4,appear:[4,35,37],append:[4,5,18,31,38],appli:[4,5,7,9,10,15,20,21,22,24,28,31,35,39],applic:[4,24,26],apply_edit:4,apply_meta_edit:7,apporach:21,approach:37,appropri:[4,5],arab:35,arbitrari:7,arbitrarili:[4,21,25,26],archiv:33,argument:[5,9,11,20,23,24,35],aris:28,arithmet:10,around:[4,35],arrai:[4,7,10,11,18,21,22,23,31,34,35,36,37],array_item:35,array_var:39,array_var_1:39,array_var_2:39,array_var_3:39,array_var_grid:39,arriv:10,as_addit:[13,34,35],as_delimited_set:35,as_df:10,as_float:35,as_int:35,as_singl:35,as_str:35,as_typ:[4,35],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,10],aspect:35,assess:[0,38],assign:[4,6,35,37],assignd:24,associ:[2,7],assum:[4,10,28,38],attach:[4,7,21,23,35],attempt:[4,34],attribut:[2,4,11,22,28,31,34,35],audit:35,auto:[4,7,34],autom:[0,7],automat:[4,13,14,20,22,23,24,31,34,35,39],auxiliari:10,avail:4,avoid:26,axes:4,axi:[2,3,4,5,7,8,10,15,19,22,35],axis_edit:[4,35],b_d:4,b_filter:34,b_name:31,back:[4,5,38],badli:[4,35],band:[4,7,18,34,35],band_numer:35,bank:3,bank_chain:3,bar:35,base:[4,5,7,8,9,10,18,20,22,31,35],base_al:10,base_text:15,based_on:[4,35],baselin:9,basi:26,basic:[20,22,25,27,35],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,7,15,28,31,34,35],batch_nam:[4,7],batchnam:[11,13],bchain:3,becaus:[24,26,35],becom:[4,10,26,35],been:[4,7,10,13,21,24,26,34,35,39],beer:38,befor:[4,7,10,13,14,24,26,31,35],begin:[4,26],behaviour:[5,8,22,26,35,39],being:[4,24,39],belong:[4,11,12,13,31,34,35,38],below:[4,10,13,26,35],benefici:21,benefit:26,better:28,between:[4,10,13,31,35],bia:10,big:21,binari:35,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,blacklist:[4,35],blank:37,blueprint:35,board:[19,24,39],bool:[4,5,7,9,10,22,38],border:4,both:[4,7,12,13,19,20,21,23,24,26,28,34,35,39],bottom3:7,bottom:35,bound:35,bracket:[7,35],brand:38,break_bi:[13,34],breakfast:22,brief:35,broader:19,buchhamm:0,bug:35,bugfix:[34,35],build:[0,3,4,5,10,15,18,19,35],built:[14,31],bunch:34,by_nam:[4,35],by_typ:[4,21,22,35],bytestr:2,cach:[4,7],calc:[5,7,8,10,31,35],calc_onli:[5,31],calcul:[4,5,7,10,14,27,34,35],call:[4,7,21,22,24,28,34,35,39],came:35,can:[2,4,5,7,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,34,35,37,38,39],cannot:[26,34,35],cap:6,carefulli:4,carri:5,case1:38,case2:38,case3:38,casedata:[4,20],cast:35,cat:[4,19,23,34,37,39],cat_nam:[7,35],categor:[4,5,7,18,22,23,27,34,35,36],categori:[4,5,7,19,22,23,34,35,37],categorized_nam:[4,35],caught:35,caus:[34,35],caution:4,cave:[19,24,39],cbase:[7,31,35],cbase_gross:35,cell:[5,10,11,26,35],cell_item:14,cellitem:35,central:5,certain:[4,22,35],chain:[0,1,3,16,22,35],chainmanag:34,chainnam:2,chang:[4,6,7,10,18,21,24,34,35],charact:[4,23,25],characterist:[19,24],chart:[0,35],check:[4,5,7,10,22,26,34,35],check_dup:35,checking_clust:7,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,7,21,24,35],clean_text:[23,35],clean_up:[4,35],clear:35,clear_factor:4,client:34,clone:[4,18],close:12,cluster:[0,1,2,7,13,14,15,35],code:[4,5,6,7,8,10,18,19,22,23,24,31,34,35,37,39],code_count:[4,22,25,35],code_from_label:[4,35],code_map:4,codes_from_nam:[4,35],codes_in_data:[4,22,35],cola:38,collect:[4,5,7,11,13,14,19,21,22,24,27,31,35],collect_cod:[7,35],colour:35,coltest:[5,7,9],column:[2,4,5,6,7,8,10,18,20,22,23,24,26,31,34,35,38,39],combin:[4,5,7,16,22,25,26,28,31,35],combind:38,come:35,comma:25,common:[8,19,24,35],commun:7,compabl:4,compar:[4,5,10,34,35],comparison:10,compat:[4,7,20,35,36],compatibilti:39,complet:[4,10,35],complex:[4,5,6,18,19,21,22,26,31,34,35],compli:[4,26],complic:18,compon:[4,5,7,8,11,15,18,21,22,23,24,35],compos:7,compound:3,comprehens:26,compress:7,comput:[0,4,5,7,9,10,27,28,31,35],concat:[2,22,35],concaten:[2,26],concern:31,cond_map:4,condit:[4,8,10,18,21,22,25,34,35],confirm:7,conflict:35,conjunct:35,connect:[4,7,12,13,35,39],consequ:[28,37],consid:[4,5,7,10,24,34,35],consist:[4,10,11,19,21,23,24,35],constitut:13,constrcut:7,construct:[4,11,13,14,19,21,23,28,31,35,39],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,35,38],content:[21,28],context:[19,23],contrast:[19,24],contributor:0,control:[4,5,7,8,10,23,24,26,35],convcrit:6,convent:[4,10,35,39],convers:[4,18,34,35],convert:[0,4,10,20,24,34,35,39],coordin:9,cope:28,copi:[2,4,9,12,18,21,23,26,34,35,39],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,35],copy_not:[4,35],copy_of_batch1:12,copy_onli:[4,35],copy_to:[4,23,35],core:[5,20,22,25,34,35,37],correct:[10,23,35],correctli:[34,35],correspond:[4,5,10,22,23,24,35,37,39],correspons:35,corrupt:34,could:[23,24,26,35],count:[4,5,7,8,10,31,34,35,38],count_not:[4,22,35],count_onli:[4,22,35],counterpart:35,counts_cumsum:[31,35],counts_sum:31,cpickl:2,crash:35,creat:[2,3,4,6,7,10,11,13,14,15,16,18,20,22,24,28,31,34,35,36],create_set:[4,35],creation:[10,18,23,35],cross:[13,28,31,35],crossbreak:28,crosstab:[4,24],crunch:35,csv:[0,4,7,11,18],cum_sum:10,cumul:[7,8,10,27,35],cumulative_sum:[7,31],current:[0,2,4,7,24,35],custom:[4,7,16,18,20,24,34,35],custom_text:[7,31],customis:35,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:10,cycl:[4,9],dai:[22,26],danish:19,data:[2,4,5,6,7,8,10,11,14,15,20,21,23,24,28,31,34,35,36,38],data_df:[4,35],data_kei:[4,7,14,28,35],datafil:[4,35],datafram:[3,4,6,7,8,10,18,19,21,22,23,28,31,34,35],datakei:35,dataset:[0,1,7,11,12,13,18,20,22,24,28,31,34,35,36,37],dataset_left:4,dataset_right:4,datasmoothi:0,datast:34,date:[4,7,19,20,21,22,23,24,35],dates_as_str:20,ddf:[0,4,35],deafult:[5,10],deal:20,decim:4,deciph:[0,18],deck:0,decod:[7,34],decode_str:7,decor:35,deep:4,deepcopi:35,defin:[2,3,4,7,10,13,14,19,21,23,28,31,34,35,37],definit:[2,4,5,7,10,14,19,24,26,27,28,34,35],definiton:[9,19,35],defint:[0,2,4,5,6,7,10,14,19,35],defintit:[21,39],defintiton:23,del:35,deleg:26,delet:[4,7,35],delimied_set:22,delimit:[4,7,19,20,21,22,23,24,25,26,34,35,37],delimited_set:[22,24,35],demograph:21,depend:[5,13,14,34,35],deprec:[7,35],deriv:[4,10,18,21,34,35,37],derivc:9,derive_categor:35,derot:[4,35,36],desc:4,descend:4,describ:[0,2,3,4,7,14,19,21,28,31,35],descript:[4,5,7,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,35,37],detail:[8,18,19],detect:[4,6,34],determin:[4,5,7],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,35],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,35,38],dictat:21,dictionari:[4,6,7,8,9,35],differ:[4,5,7,8,10,13,19,20,23,24,28,31,34,35,36,38],digest:19,dim:[5,10,31],dim_comp:35,dimens:[0,4,5,10,18,35,36],dimension:[4,35],dimensionizing_mapp:4,dimensions_comp:[4,20,35,39],dimlabel:35,dinner:22,direct:26,directli:[2,4,22,35],discret:[7,13],disk:[4,21,35],dispers:5,distinct:[19,31],distribut:[5,10],div:[7,35],dive:[19,24,39],divid:10,dk_filter:4,dms:4,dmsrun:4,doc:[22,35],docstr:6,document:[0,4,7,19,20,23,35],doe:[4,11,23,26,35],doesn:[4,25,26],dog:19,don:[19,22,24,37],done:[4,13,14],doubl:22,down:39,downbreak:[28,31,35],download:39,draft:35,draw:[21,24],drawn:[4,35],drink:38,drink_1:38,drink_2:38,drink_level:38,driven:0,drop:[4,5,7,10,21,35],drop_cod:[7,35],drop_delimit:4,drop_dupl:4,drope:[4,38],dropna:[4,6,35,38],dropx:4,dto:39,dtype:[20,22,26,35],due:34,dump:20,dupe:34,duplic:[4,23,34,35],durat:22,dure:[4,7,10,24],each:[2,4,5,6,7,13,14,19,23,24,28,31,35,38,39],eagleston:0,eas:35,easi:[0,19,22,23,35],easier:[12,22,28,34],easiest:22,easili:[4,12,20,31,35],ebas:[7,35],echo:4,ect:35,edit:[0,4,8,10,13,14,15,18,19,21,24,26,35],edit_param:8,eff:10,effect:[6,10,26,35],effici:35,ein:[19,23],either:[4,5,10,13,19,21,22,23,26,35],element:[4,7,10,19,35,39],eleph:4,els:26,emploi:19,empti:[4,7,22,24,26,34,35],empty_item:[4,35],en_u:7,enabl:[23,35],encod:[4,34,35],encount:20,end:[2,4,7,11,12,34,35],end_tim:22,enforc:35,eng:35,engin:[1,27,34],english:[19,35],enhanc:34,enough:4,enrich:35,ensur:[4,11,35],enter:[4,14],entir:[4,10,20,35],entri:[4,6,7,10,19,26,35],enumer:[4,7,23,24,26,37],environ:4,eponym:35,eqaul:4,equal:[4,10,35],equip:31,equival:[4,39],eras:[4,7],error:[4,10,34,35],escap:4,especi:[22,26,31],estim:10,etc:[4,19,21,28,31,35],ethnic:[13,22,28,31],even:[4,31,35],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,35],exactli:[4,24,25],exampl:[2,4,5,7,11,18,19,20,22,24,25,28,31,35,38,39],excel:[0,3,4,5,35],except:[4,21,35],exchang:[10,23],exclud:[4,5,7,8,10,31,35],exclus:[4,25,35],execut:4,exercis:[22,24],exist:[4,7,10,12,13,18,21,23,24,26,31,34,35],expand:[4,7,8,10,31],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,7,35,37],explor:19,expos:23,express:[4,5,6,7,10,22,25,35],ext_item:[4,35],ext_valu:4,ext_xk:35,ext_yk:35,extend:[4,5,7,13,18,22,31,35],extend_cod:[7,35],extend_filt:14,extend_i:[13,35],extend_item:[4,35],extend_valid_tk:35,extend_valu:[4,23,35,37],extend_x:[34,35],extens:[3,4,7,19,20,35],extra:35,extract:14,extrapol:25,factor:[4,6,7,35],factor_label:[7,35],factormap:4,fail:[4,35],failur:20,fall:[4,5,10],fallback:8,fals:[4,5,7,10,13,20,22,23,24,26,31,34,35],fast_stack_filt:35,favour:10,featur:[19,35],feed:[4,35],feedback:4,femal:[4,22],few:[11,21,22,35,38],figur:10,file:[2,3,4,5,7,11,20,21,22,28,35,39],file_nam:20,filenam:[2,7],fill:[4,18,34,35],fillna:[4,18],filter1:35,filter2:35,filter:[4,6,7,10,11,15,18,28,31,34,35],filter_1:35,filter_2:35,filter_def:6,filter_kei:[7,14],filter_nam:35,find:[4,11,22,28,34,35],find_duplicate_text:4,finish:[4,21,35],finnish:19,first:[4,5,10,13,19,21,26,31,35],fit:[19,23,24],fix:[4,15,35],flag:[4,5,35],flag_bas:10,flat:4,flatten:[4,24,35],flexibl:[6,7,24],float64:20,folder:20,follow:[4,8,10,11,14,15,19,20,21,22,23,24,26,28,31,35,38,39],folow:26,footbal:24,forc:[26,35],force_text:[4,23,35],forcefulli:4,form:[3,4,7,10,14,20,23,24,25,26,28,35],format:[0,4,6,10,19,20,23,26,31,35,38],former:[4,7,22,23,24,35],fortnight:22,found:[2,3,4,7,10,20,22,24,26,31,34,35],four:[28,31],frang:[4,25,26,35,38],freez:7,freeze_master_meta:7,french:19,frequenc:[4,5,8,9,10,28,31,35],freysson:0,from:[0,2,3,4,5,7,10,12,14,15,18,19,21,24,25,26,28,31,34,35,38],from_batch:[4,35],from_compon:[4,20,35],from_dichotom:[4,35],from_excel:[4,35],from_sav:7,from_set:[4,20,35],from_stack:[4,35],front:35,fulfil:4,full:[3,4,5,7,21,23,25,35,39],fullnam:31,fully_hidden_arrai:4,fun:35,further:21,futur:[7,35],geir:0,gender:[4,13,14,15,22,24,26,28,31,35,38],gener:[2,4,5,7,8,9,11,20,22,23,26,31,35],generate_report:35,german:[19,23],get:[4,7,8,11,12,13,14,22,28,31,35,37],get_batch:[4,12,13,28,31,34],get_edit_param:8,get_properti:4,get_qp_dataset:35,get_s:10,get_sig:10,get_statist:10,get_std_param:8,getter:34,give:[13,26,35],given:[4,5,6,7,10,20,26,34,35],global:[4,8,10,14,15,21,35],goe:4,going:37,grab:34,greater:4,grid:[4,35],griffith:0,group:[4,5,6,7,8,10,19,21,22,24,25,28,31,35],group_nam:6,group_target:6,grouped_ag:24,grp:10,grp_text_map:31,guid:35,gzip:7,hack:4,had:26,hand:24,handl:[0,6,7,9,10,20,31,34,35,37,39],handler:35,happen:[4,24],happend:4,has:[2,4,7,12,15,19,21,25,26,31,34,35,38],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:8,have:[4,7,10,13,20,23,24,25,26,28,34,35,38],head:[20,22,23,24,26,35,39],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,7,26,28,35,39],hidden:[4,34,35],hide:[4,15,34,35],hide_empti:35,hide_empty_item:[4,35],hide_on_i:4,hide_valu:[4,35],high:[5,10],higher:10,hmerg:[4,34,35],hockei:24,hold:[2,4,7,10,34],horizont:[4,18],household:20,how:[3,4,7,14,19,22,24,28,31,37,39],howev:[23,24,26,37,39],hrafn:0,html:[4,23,35],hub:34,ident:[4,20,24,34,35],identif:4,identifi:[4,5,23,35,38],ids:4,ignor:[4,7,10,22,24,35],ignore_arrai:4,ignore_cod:4,ignore_flag:10,ignore_item:[4,24,35],ignore_valu:[4,24],ill:23,implement:[7,10,19,22,35],impli:4,implicitli:10,impract:28,impute_method:6,incl:[7,35],includ:[2,3,4,5,7,10,13,25,26,28,31,34,35,38],inclus:[24,35],incom:[4,10,24],inconsist:[4,15,23,34,35],incorrect:35,incorrectli:35,independ:[4,10],index:[0,2,4,7,10,35],indic:[4,5,7,10,23,24,26,35],individu:[3,4,35],industri:20,infer:[7,20,24,35],info:[4,18,19],inform:[0,4,8,9,13,14,15,19,20,23,26,31,35,37,38],inherit:[11,35],inhomogen:10,init:26,initi:[4,18,35,38],inject:[4,26],innermost:8,inplac:[4,7,10,18,21,26,35],input:[4,6,7,10,11,20,35],insert:[4,13,35],insid:[2,4,9,10,19,20,22,23,35],inspect:[4,18,35],instal:[4,35],instanc:[2,3,4,7,9,10,11,14,20,21,22,23,24,28,31,34,35],instead:[4,7,10,20,25,26,34,35],instruct:[5,10,19,26,31,34,35],int64:[20,22],integ:10,integr:22,intend:7,inter:[4,6],interact:[2,3,4,21,22,35],interfac:0,interim:35,interlock:[4,18,35],intern:[2,34,35],interpret:[4,25],intersect:[4,18,22,35],intro:21,introduc:10,involv:26,iolocal:[7,20],ioutf8:7,ipython:[4,21,35],is_arrai:[31,35],is_bas:8,is_block:31,is_count:8,is_cumul:8,is_dat:35,is_delimited_set:35,is_float:35,is_g:[4,25,35],is_int:35,is_like_numer:[4,22,24,35],is_meanstest:8,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:8,is_pct:8,is_propstest:8,is_singl:35,is_stat:8,is_str:35,is_sum:8,is_weight:[8,31],isol:35,issu:[20,34,35],ist:[19,23],item:[4,10,11,13,19,20,21,22,23,24,31,35,37,38,39],item_nam:[4,35],item_no:[4,35],item_text:[4,22,35],iter:[9,22,35],its:[2,4,5,7,8,10,11,12,13,14,15,19,20,22,24,25,26,31,35,38,39],itself:[4,10],jame:0,jjda:20,jog:24,join:[4,22],json:[4,7,11,18,35],jupyt:[4,21,35],just:35,keep:[4,9,10,21,23,24,35,38],keep_bas:10,keep_cod:[10,35],keep_origin:4,keep_variable_text:[4,35],kei:[2,4,5,7,8,11,14,19,23,24,26,31,35,38],kept:[4,10,11,35],kerstin:0,keyword:[5,9,35],kind:[22,24,28,35],kite:[19,24,39],know:[14,19,22,24,37,39],kritik:31,kwarg:[4,5,8,9,35],lab:7,label:[4,5,7,14,19,20,22,23,24,26,34,35,37,38,39],lack:0,lang:19,languag:[11,18,23,35],larg:[14,21,28,31,35],last:4,lastli:34,later:[4,13,20,35],latest:[0,33,35],latter:[22,35],lead:[24,31,35],least:[4,22,23],leav:[23,24],left:[4,10,34,35],left_id:4,left_on:[4,35],legaci:4,lemonad:38,length:[4,35],less:[22,34,38],let:[23,24,28],level:[4,5,7,10,14,19,26,31,35,38],lib:[4,19,35,39],librari:[0,19,35],lift:24,like:[0,4,5,7,13,14,19,21,22,24,26,28,31,34,35,38,39],limit:[4,10,24,26,35],link:[2,4,5,7,8,9,10,11,14,27,31,35],list:[2,4,5,6,7,9,10,13,18,19,21,22,23,24,26,34,35,37,38],list_vari:35,listen:26,load:[2,3,4,7,11,35],load_cach:7,loc:31,local:[7,13,22,28,31],locat:[2,4,7,35],logic1:35,logic2:35,logic:[4,5,8,10,18,22,24,26,34,35],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:35,look:[4,13,24,26,34,35,38,39],loop:[35,38],lose:[13,24,38],lot:[4,21,22,26],low:[4,5,10,34],lower:[4,10,34,35],lower_q:10,lunch:22,machin:[4,20,35],made:2,mai:[4,25,26],main:[4,6,10,13,14,19,24,31,34,35],main_filt:35,mainli:3,major:10,mak:35,make:[4,5,22,24,28,35],make_summari:[13,35],make_templ:9,male:[22,26],manag:[0,18,26],mani:[13,28],manifest:4,manipul:[10,13,15],manual:[4,5,35,37],map:[4,5,6,7,9,10,18,20,23,35,37],mapper:[4,18,25,35,38],mapper_to_meta:4,margin:[5,10],mark:10,market:[0,20],mask:[4,18,21,22,23,35,37,38,39],mass:5,massiv:26,master:[34,35],master_meta:7,match:[4,6,7,20,24,35,39],matric:10,matrix:10,matrixcach:7,matter:[11,34],max:[7,10,31,35],max_iter:6,mdd:[0,4,20,35],mdm_lang:35,mean:[4,5,6,7,8,10,13,15,20,22,24,25,26,31,35,38],measur:[5,10],median:[6,7,10,31,35],membership:[22,35],memori:[21,35],memoryerror:35,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,34,35],merge_exist:[4,34],merge_text:[4,35],messag:35,meta:[4,5,7,8,11,12,14,15,18,20,21,22,24,26,31,34,35,37,38,39],meta_dict:[4,35],meta_edit:[4,7,15],meta_to_json:[4,35],metadata:[0,4,7,18,19,20,24,26,34,35,37,39],metaobject:35,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,34,35,37,38,39],metric:[5,7,10],mid:[5,10,22,35],middl:35,might:[7,21,23,24,34],mimic:[5,10],mimick:[5,10,22],min:[4,7,10,31,34,35],min_value_count:[4,34],minimum:5,minor:35,mismatch:35,miss:[4,6,8,14,20,22,23,24,34,37,39],missing_map:[4,35],mix:[4,24],mode:[6,7,20,35,36],modifi:[4,7,10,15,24,34,35],modu:35,modul:[0,4,7],month:22,more:[2,4,22,25,26,31,34,35],morn:22,most:[10,21,22,26,34],mous:4,move:[0,4,21,35],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,38],mul:7,multi:[5,10,19],multiindex:10,multipl:[0,3,4,5,19,22,23,25,26,35],multipli:10,multivari:10,must:[4,6,7,20,21,23,26,31,35,38],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,34,35,37,38,39],name_data:[35,39],nan:[4,10,20,22,23,24,26,28,31,35,39],nate:4,nativ:[0,4,18,35],natur:[10,21,22],necessari:[10,21,35,38],need:[4,7,9,10,21,23,24,34,35,39],neg:35,nest:[7,8,19,25,26],net:[5,7,8,10,18,27,34,35],net_1:[7,31],net_2:[7,31],net_3:7,net_map:[7,31],net_view:35,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:35,new_cod:10,new_column:4,new_d:35,new_data:7,new_data_kei:7,new_dataset:4,new_int:23,new_meta:7,new_nam:[4,23,24,34,35],new_ord:[4,21,35],new_rul:35,new_set:[4,35],new_singl:23,new_stack:2,new_text:[4,23],new_var:35,new_weight:7,newli:34,next:[4,7,11,19,31,35,39],no_data:24,no_filt:[4,7,28,31,35],non:[4,7,22,25,27,35],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,34,35,37,39],none_band:35,nonea:4,normal:[4,5,7,10,35],norwegian:19,not_al:18,not_ani:[4,10,18,35],not_count:18,notat:[5,7,8,10],note:[2,4,5,26,31],notebook:[4,21,35],notimplementederror:[15,35],now:[13,14,21,31,34,35,38],num:5,number:[4,6,10,20,22,23,24,34,35],numer:[4,5,7,18,19,23,24,31,34,35,37],numpi:[0,10,35],obei:22,object:[2,3,4,5,7,10,18,20,21,24,26,28,31,35,37],obscur:26,observ:0,obvious:31,occur:26,oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,35,37,39],often:[21,22,24],old:[4,5,7,35],old_cod:10,old_nam:35,older:35,omit:26,omnibu:34,on_var:[7,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,34,35],ones:[4,10,15,21,22,34],onli:[4,7,9,10,11,13,14,21,23,24,26,28,31,34,35,37,38],only_men:24,only_typ:[4,7],onto:26,oom:35,open:[0,11,12,34,35],oper:[4,5,20,22,24,25,26,35],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,7,10,13,14,19,22,23,31,34,35],order:[2,4,11,18,19,24,34,35],ordereddict:[3,4,7,13,28,31],organ:16,orgin:10,orient:[2,35],origi:4,origin:[4,7,10,21,24,34,35],other:[4,5,7,10,11,12,13,19,23,24,25,26,34,35,38],other_sourc:[7,31,35],otherwis:[4,34,35],our:[0,22,26],out:[5,12,14,24,31,35],outcom:4,outdat:35,output:[4,7,10,13,22,35],outsid:35,over:[4,7,9,10,35],overcod:[4,7,31],overlap:10,overview:[4,7,22,28,35],overwrit:[4,15,21,26,34,35],overwrite_margin:10,overwrite_text:4,overwritten:[4,7,35],ovlp_correc:10,own:[13,35],pack:4,packag:5,paint:[3,35],painter:35,pair:[4,5,10,18,23,26,35,37],panda:[0,3,4,6,7,10,19,20,22,26,35],pane:4,parachut:[19,24,39],parallel:7,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,34,35,38],parent:[4,19,20,35],pars:5,part:[4,8,19,21,22,26,35],parti:18,particip:20,particular:4,pass:[4,5,7,10,21,22,24,34,35],past:26,path:[2,3,4,7,20,35],path_clust:3,path_csv:20,path_data:[4,39],path_ddf:[4,20,35],path_json:20,path_mdd:[4,20,35],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:7,path_txt:20,path_xlsx:[4,35],path_xml:20,pct:4,peopl:20,per:[4,5,6,7,10,12,20,22,23,24,26,35,39],percentag:[7,8,10,31,35],perform:[4,5,7,8,10,21,26,31,34,35],perman:21,physic:35,pick:[4,24],pickl:2,pilat:24,pivot:7,place:[10,26,34,35],plai:22,plain:[0,8,20],plan:[11,13,14,19,24,28,31],pleas:[7,21,22,34,35],point:19,pointer:19,pool:10,popul:[4,11,13,14,27,31,35],portion:7,posit:[4,10,21,22,23,28,34,35],possibl:[3,4,5,7,12,13,19,22,23,25,31,34,35,38],power:0,powerpoint:[5,35],powerpointpaint:35,pptx:35,pre:[4,26,31],precis:[26,35],prefer:22,prefix:[4,7],prep:25,prepar:[3,21,23,31,34,35],present:[4,10,35],preset:[4,7],pretti:[4,26],prevent:[4,15,21,23,24,34,35],previou:[21,35],previous:[4,35],primarili:4,print:[13,22,28,31,35],prior:[4,24],prioriti:35,probabl:[19,24,35],problem:[34,35],process:[0,4,7,9,20,21,22],produc:[5,10,13,24],product:[35,38],profession:[4,35],progress:[4,35],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[34,35,39],properli:39,properti:[4,10,11,20,35],proport:[5,6,8],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,35,37],proxi:7,purpos:19,put:7,python:[4,34],q01_1:4,q01_3:4,q11:35,q11_grid:35,q12:38,q12_10:38,q12_11:38,q12_12:38,q12_13:38,q12_1:38,q12_2:38,q12_3:38,q12_4:38,q12_5:38,q12_6:38,q12_7:38,q12_8:38,q12_9:38,q12_:38,q12a:38,q12a_10:38,q12a_11:38,q12a_12:38,q12a_13:38,q12a_1:38,q12a_2:38,q12a_3:38,q12a_4:38,q12a_5:38,q12a_6:38,q12a_7:38,q12a_8:38,q12a_9:38,q12a_grid:38,q12b:38,q12b_10:38,q12b_11:38,q12b_12:38,q12b_13:38,q12b_1:38,q12b_2:38,q12b_3:38,q12b_4:38,q12b_5:38,q12b_6:38,q12b_7:38,q12b_8:38,q12b_9:38,q12b_grid:38,q12c:38,q12c_10:38,q12c_11:38,q12c_12:38,q12c_13:38,q12c_1:38,q12c_2:38,q12c_3:38,q12c_4:38,q12c_5:38,q12c_6:38,q12c_7:38,q12c_8:38,q12c_9:38,q12c_grid:38,q12d:38,q12d_10:38,q12d_11:38,q12d_12:38,q12d_13:38,q12d_1:38,q12d_2:38,q12d_3:38,q12d_4:38,q12d_5:38,q12d_6:38,q12d_7:38,q12d_8:38,q12d_9:38,q12d_grid:38,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,38],q1_2:[4,26,38],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:35,q4a_1:35,q4a_2:35,q4a_3:35,q4a_grid:35,q5_1:[19,21,22,24,35,39],q5_2:[19,21,22,24,35,39],q5_3:[19,21,22,24,35,39],q5_4:[19,21,22,24,35,39],q5_5:[19,21,22,24,35,39],q5_6:[19,21,22,24,35,39],q5_grid:39,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:39,q6_net:31,q6copi:39,q6new:39,q6new_grid:39,q6new_q6copi:39,q6new_q6copy_grid:39,q6new_q6copy_tran:39,q6new_q6copy_trans_grid:39,q7_1:[21,22,35],q7_2:[21,22,35],q7_3:[21,22,35],q7_4:[21,22,35],q7_5:[21,22,35],q7_6:[21,22,35],q7_grid:39,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:38,q_label:[4,35],qtp:38,qtype:[4,23,34,37,39],qualifi:[4,5,10],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,35,37,39],quantipyview:[1,7,35],quantiti:[10,35],queri:[2,4,6,7,18,19,24,35,39],question:[4,10,19,24,26,31,35,38],questionnair:21,quick:[4,22,35],quickli:[6,21,22,24,35],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,34,35],rake:35,rang:[4,5,18,21,35],rate:[35,38],raw:[5,10],raw_sum:10,rbase:7,read:[0,4,20,35],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,35,39],read_spss:[4,20],rebas:10,rebuild:24,rec:[4,35],receiv:38,recod:[0,4,7,18,35],recode_from_net_def:7,recode_seri:4,recoded_filt:35,recommend:24,record_numb:[13,22],reduc:[4,7,10,21,35],reduced_d:21,reduct:4,refactor:35,refer:[4,7,10,19,23,26,28,31,35],referenc:[7,13,19,26,35],reflect:[4,10,21,35],refresh:7,refus:[19,24],regard:[4,35],region:[4,35],regist:[4,22,35],regroup:[4,34],regular:[4,19,31,35],regularli:[22,23,24],reindex:4,reintroduc:35,rel:21,rel_to:8,relat:[2,8,23,26,35],relation_str:8,relationship:7,relev:[23,35],religion:22,reload:[21,35],remain:[4,5,21,26,35],rememb:37,remind:37,remov:[4,5,7,10,12,13,18,34,35],remove_data:7,remove_filt:35,remove_html:[4,35],remove_item:4,remove_valu:[4,34],renam:[4,18,24,34,35,39],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,34,35],reorder_item:[4,35],reorder_valu:4,reorgan:0,repair:[4,34,35],repair_text_edit:[4,35],repeat:[21,28],repetit:26,replac:[4,10,13,23,26,31,35],replace_i:[13,35],replace_text:[4,35],report:[0,4,5,6,34,35],reposit:[4,21,35],repres:[4,26],represent:[7,8,10,19,24,34],request:[5,7,13,21,23,26,31,35],request_view:35,requir:[4,21,23,35,39],rescal:[5,7,8,10,31],research:[0,20],reset:[4,35],reset_index:4,resid:35,resolv:34,resolve_nam:[4,34],resp:34,respect:[4,10,24,34,35,37],respond:[10,21,38],respons:[19,22,25,26,35,38],responsess:35,restor:[4,7,21,35],restore_item_text:4,restore_meta:7,restrict:[4,5,7,10,19,21,22,35],result:[3,4,5,7,9,10,16,20,22,23,24,26,28,35],result_onli:10,retain:9,retriev:10,revers:[24,25],revert:[4,21,35],right:[4,34,35],right_id:4,right_on:[4,35],rim:[1,4,35],roll:4,roll_up:4,rollback:[18,35],rolled_up:4,round:6,row:[4,5,10,18,20,22,35,39],row_id:4,row_id_nam:4,rule:[4,35,39],run:[4,7,10,15,24,28,31,35],safe:[4,23],safeguard:4,sai:26,same:[3,4,7,13,19,20,22,26,28,34,35,38,39],sampl:[5,8,10,34,35],sample_s:14,sandbox:35,satisfi:35,sav:[4,7,20],save:[2,3,4,7,21,28,35],savepoint:18,scalar:35,scale:[5,6,10,19,35,38],scan:4,scenario:39,scheme:[4,6,19,35],scratch:[18,35],script:4,search:4,second:[4,5,10,15,31],sect:4,section:[8,10,11,14,21,26],see:[13,21,24,26,28,31,35],seen:[26,39],segemen:26,segment:18,select:[4,7,9,13,14,21,22,31,35,38],select_text_kei:4,self:[2,4,7,10,26,28,35],sem:[7,10],semi:34,sensit:[4,34],separ:[4,26,37],septemb:33,sequenc:4,seri:[4,19,22,26,35],serial:2,session:[21,35],set:[3,4,5,6,7,9,10,11,12,13,18,19,20,22,23,24,26,28,34,35,37],set_cell_item:14,set_col_text_edit:35,set_column_text:35,set_dim_suffix:35,set_encod:4,set_factor:4,set_item_text:[4,23,35],set_languag:14,set_mask_text:35,set_miss:[4,35],set_opt:35,set_param:10,set_properti:[4,15],set_sigtest:[14,31,35],set_target:6,set_text_kei:4,set_unwgt_count:35,set_val_text_text:35,set_value_text:[4,15,23,35],set_variable_text:[4,15,23,35],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,35],setup:[4,10,13,35],sever:[5,22,38],shape:[4,21,31],share:[4,19],sheet:[4,35],shop:4,short_item_text:35,shorten:[4,35],shorthand:[4,5],shortnam:[5,8],should:[3,4,7,14,21,22,26,34,35,38,39],show:[4,10,13,19,21,22,31,35],shown:[4,10,34,35],side:[9,35],sig:[5,7,10,14,35],siglevel:35,signific:[5,10,11,27],significancetest:35,sigproperti:35,similar:[28,38],similarli:[22,23],similiar:23,simpl:[5,6,10,19,25,35,37],simpli:[4,22,23,24,25,31,34,35],simplifi:[24,26],sinc:[10,26,31,39],singl:[3,4,7,19,20,21,22,23,24,26,34,35,37,39],sit:26,six:22,size:[5,8,10,21,22,31,35],skip:[22,23,31,35],skip_item:35,slice:[4,8,15,18,35],slicer:[4,18,22,24,35],slicex:4,small:[5,35],snack:22,snapshot:[4,21,35],snowboard:[19,24,39],soccer:24,social:0,softwar:[0,4,5,10,20,34],solut:35,solv:35,some:[13,14,15,22,25,26,34,35,38],someth:38,sometim:[21,28,34],soon:35,sorri:0,sort:[4,15,35],sort_by_weight:4,sortx:4,sourc:[0,4,7,19,20,22,23,35,39],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:8,specfic:14,special:[0,11,14,19,28,31,35],specif:[3,4,5,7,8,10,11,13,14,15,19,21,23,35,39],specifi:[2,4,5,6,7,10,13,20,23,31,35,37],speed:35,spell:[4,34],split:[4,7,13,35],split_view_nam:7,sport:[20,22],spreadsheet:0,spss:[0,4,10,18,35],spss_limit:[4,35],squar:7,stack:[1,2,3,4,5,8,11,13,14,27,31,35],stage:[4,35],standalon:18,standard:[8,10,20,34],standardli:24,start:[4,18,23,24,26],start_meta:[4,35],start_tim:22,stat:[4,5,7,10,31,35],state:[4,15,18,24,35],statement:[4,5,19,25,26],statisfi:35,statist:[0,4,5,7,8,10,18,27,28,35],std_paramet:8,stddev:[7,10,31,35],ste:35,stem:35,step:[31,37],still:[26,35],store:[4,5,7,11,12,13,19,21,24,28,31,35],store_cach:7,str:[3,4,5,6,7,8,9,10,24,34,35,37,38],str_tag:[4,34],strict:[4,35],strict_select:9,strictli:23,string:[2,4,6,7,8,10,19,20,21,22,23,24,25,34,35],strip:35,structur:[0,4,6,7,9,11,13,19,20,21,24,28,35,38],studi:35,style:[4,7],sub:[7,31],subclass:[2,11],subclasss:15,sublist:35,subset:[4,9,18,22,24,35],subset_d:4,substr:[4,34],subtl:35,subtyp:[19,35,39],suffix:[4,5,24,34,35,39],sum:[4,5,7,8,10,27,35],summar:[4,5,10,35],summari:[4,5,6,7,8,10,13,22,31,34,35],summaris:7,summat:10,suppli:24,supporintg:7,support:[0,7,8,18,19,22,23,24,34,35,39],surf:[19,24,39],survei:21,sv_se:[31,35],swap:[7,8,10,35],swedish:[19,35],swim:24,syntax:35,sys:4,tab:20,tabl:[0,7],tabul:[5,13,28],tag:[4,23,34,35],take:[4,5,7,11,22,24,25,26,34,35,38],taken:[4,7,14,15,24,34,35],target:[4,6,18,23,34,35],target_item:4,task:22,team:22,temp:4,templat:[5,9,35],temporari:[4,35],temporarili:4,ten:26,tend:4,term:[7,23,35],termin:35,test:[2,4,5,8,10,11,22,27,34,35,37],test_cat_1:37,test_cat_2:37,test_cat_3:37,test_tot:[5,10,35],test_var:[34,35,37],testtyp:10,text1:20,text:[4,5,7,8,14,18,20,21,22,24,26,31,35,37,39],text_kei:[3,4,7,11,18,22,23,31,35],text_label:[4,35],text_prefix:7,textkei:[4,35],than:[4,22,24,34,35],thei:[4,10,13,14,20,25,26,31,34,35],them:[4,5,13,20,22,26,31,34,35],themselv:[4,10],therefor:[4,5,24,34,35],thi:[2,3,4,5,6,7,10,13,14,15,20,21,22,23,24,26,28,31,35,38,39],third:18,thorugh:24,those:4,three:[4,21,22,24,26,35,37],threshold:5,through:[2,3,4,9],throughout:[4,19,20,35],thu:6,time:[7,19,21,22],titl:13,tks:35,to_arrai:[4,34,35,38],to_delimited_set:[4,35],to_df:10,to_excel:7,todo:[4,5,6,7,9,10],togeth:[3,4,7,19,21],toggl:7,too:35,tool:[5,20,24,25],top2:31,top3:7,top:26,topic:[19,34,35],total:[4,5,6,10,13,35],toward:35,tracker:35,tradit:10,transfer:35,transform:[0,4,5,10,18,35],translat:23,transpos:[4,13,24,31,39],transpose_arrai:13,transposit:24,treat:[4,10,25,31],tree:28,treshhold:10,trigger:4,tstat:10,tupl:[4,8,23,24,35,37],turn:19,two:[4,5,10,13,19,21,23,28,31,34,35],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,34,35,37,39],type_nam:7,typic:26,ultim:4,unabbrevi:25,unattend:4,unbind:4,uncod:[4,35],uncode_seri:4,uncodit:5,unconditi:10,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:7,uni:[5,10],unicod:7,unifi:[4,35],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,7,24,28,31,35],unique_id:[4,22],unique_kei:[4,35,38],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:10,unqiu:24,unrol:[4,31,35],untouch:[23,34],unusu:35,unwant:[4,35],unweight:[7,10,31,35],unweighted_bas:[7,31,35],unwgt:35,upcom:33,updat:[4,8,9,10,23,34,35],update_axis_def:[10,35],update_exist:[4,35],upon:19,upper:[4,35],upper_q:10,uppercas:34,usag:[23,34,35],use:[0,2,4,5,7,10,12,13,19,20,21,22,23,24,26,34,35,36],use_ebas:10,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,34,35],useful:[21,22,35],user:[2,4,14,35,37],userwarn:[35,37],uses:[4,10,35],using:[0,2,3,4,6,7,19,20,21,24,25,26,28,31,34,35,39],usual:19,utf8:34,utf:7,val:4,val_text_sep:4,valid:[4,6,7,14,19,24,26,31,35,37],valid_cod:35,valid_tk:[11,35],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,34,35,36,39],value_count:[4,22,35],value_map:38,value_text:[4,22,35],valueerror:[4,21,23,34,35],var_exist:[22,35],var_grid:38,var_nam:[35,38],var_suffix:4,varcoeff:10,vari:22,variabl:[0,4,5,6,7,8,10,11,18,19,20,23,27,34,35,37,38,39],variable_typ:7,variables_from_set:35,varianc:10,variant:[22,34],varibal:38,varibl:35,variou:[5,14,22,28,31],varlist:[4,35],varnam:[4,35],vector:10,verbatim:[11,34],verbos:[4,7,25,31,35],veri:[19,23,24,35],versa:10,version2:24,version:[4,5,7,10,19,21,23,24,26,34,35],versu:31,vertic:[4,18],via:[0,4,5,7,21,22,23,24,31,35],vice:10,view:[1,2,3,4,5,7,9,10,14,16,22,27,28,35],view_kei:31,view_name_not:10,viewmanag:35,viewmapp:[1,7],viewmeta:8,visibl:[31,35],vmerg:[4,34,35],wai:[7,12,13,19,21,22,23,26,31,35,36],wait:21,want:[21,24,26],warn:[4,31,34,35],water:38,wave:21,weak:[4,34],weak_dup:4,week:22,weight:[0,4,6,7,8,10,11,12,22,24,31,34,35],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,weigth:4,well:[4,10,20,22,25,26,31,34,35,39],went:35,were:[26,34,35],wgt:35,what:[16,19,20,24,26,27,35,36,39],whatev:[4,26],when:[4,5,7,10,20,21,23,24,26,35,39],where:[2,3,4,10,24,25,26],whether:[5,10],which:[4,5,7,10,11,13,14,15,22,23,24,26,28,31,34,35,38],whole:[4,35],whose:[4,7,35],wide:35,wil:4,wildcard:26,window:20,windsurf:[19,24,39],wise:[4,31],witch:35,within:[4,10],without:[34,35],women:15,work:[4,11,21,23,31,35],workbook:3,workspac:35,world:20,would:[4,19,24,26,35],wouldn:[19,24],wrap:35,wrapper:[4,10,34],write:[4,7,20,21,35,39],write_dimens:[4,34,35],write_quantipi:[4,21],write_spss:[4,20],writen:38,written:[21,35],wrong:35,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:10,xks:[4,7,31,35],xlsx:4,xml:[4,20],xsect:10,xtotal:4,y_filter:35,y_kei:[14,28,31],y_on_i:[13,28,31,35],year:[19,22,39],yes:20,yet:34,yield:22,yks:[4,35],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,35,38,39],younger:24,your:[4,19,20,21,24,26,35],ysect:10},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","Stack","View","ViewMapper","quantify.engine","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Documentation","Release notes","Upcoming (September)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,39],Using:20,about:38,access:39,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:35,arrai:[13,19,24,38,39],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,37],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:39,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,37,39],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,39],datafram:20,dataset:[4,15,19,21,23,38,39],deciph:20,definit:31,deriv:26,derot:38,descript:31,detail:26,dice:22,differ:37,dimens:[20,39],document:32,edit:23,end:13,engin:[10,29],exampl:26,exist:[22,25],extend:23,featur:0,fill:26,fillna:26,filter:[14,21],from:[20,23],has_al:25,has_ani:25,has_count:25,horizont:21,how:[36,38],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:34,link:28,list:25,load:12,logic:25,manag:21,map:19,mapper:26,mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:39,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[33,35],numer:26,object:[19,22,23],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,process:18,properti:14,python:0,quantifi:10,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,releas:[33,35],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:34,set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:36,special:13,spss:20,stack:[7,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:34,use:38,valu:[19,22,23,37],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[8,17,31],viewmapp:9,wai:37,weight:14,what:[17,28,38]}}) \ No newline at end of file diff --git a/docs/API/_build/html/sites/api_ref/00overview.html b/docs/API/_build/html/sites/api_ref/00overview.html new file mode 100644 index 000000000..fd749a574 --- /dev/null +++ b/docs/API/_build/html/sites/api_ref/00overview.html @@ -0,0 +1,439 @@ + + + + + + + + + + + API references — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                  + + + + +
                  + + + + + + +
                  +
                  + + + + + + +
                  + +
                  +
                  +
                  +
                  + + + + +
                  +
                  + + +
                  +
                  + +
                  + +
                  + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/api_ref/Chain.html b/docs/API/_build/html/sites/api_ref/Chain.html index 778a0482d..61348a063 100644 --- a/docs/API/_build/html/sites/api_ref/Chain.html +++ b/docs/API/_build/html/sites/api_ref/Chain.html @@ -92,9 +92,11 @@

                  Quick search

      Parameters:
        -
      • spec (dict) – The banked chain specification object.
      • -
      • text_key (str, default='values') – Paint the x-axis of the banked chain using the spec provided +
      • spec (dict) – The banked chain specification object.
      • +
      • text_key (str, default='values') – Paint the x-axis of the banked chain using the spec provided and this text_key.
      Returns:

      bchain – The banked chain.

      +
      Returns:

      bchain – The banked chain.

      Return type:

      quantipy.Chain

      @@ -398,7 +405,7 @@

      Cluster

      Parameters:path_cluster (str) – The full path to the .cluster file that should be created, including +
      Parameters:path_cluster (str) – The full path to the .cluster file that should be created, including the extension.
      Returns:
      Parameters:path_cluster (str) – The full path to the .cluster file that should be created, including +
      Parameters:path_cluster (str) – The full path to the .cluster file that should be created, including the extension.
      Returns:
      +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'].
      • +
      • qtype ({'int', 'float', 'single', 'delimited set', 'date', 'string'}) – The structural type of the data the meta describes.
      • +
      • label (str) – The text label information.
      • +
      • categories (list of str, int, or tuples in form of (int, str), default None) – When a list of str is given, the categorical values will simply be +enumerated and mapped to the category labels. If only int are +provided, text labels are assumed to be an empty str (‘’) and a +warning is triggered. Alternatively, codes can be mapped to categorical +labels, e.g.: [(1, 'Elephant'), (2, 'Mouse'), (999, 'No animal')]
      • +
      • items (list of str, int, or tuples in form of (int, str), default None) – If provided will automatically create an array type mask. +When a list of str is given, the item number will simply be +enumerated and mapped to the category labels. If only int are +provided, item text labels are assumed to be an empty str (‘’) and +a warning is triggered. Alternatively, numerical values can be +mapped explicitly to items labels, e.g.: +[(1 'The first item'), (2, 'The second item'), (99, 'Last item')]
      • +
      • text_key (str, default None) – Text key for text-based label information. Uses the +DataSet.text_key information if not provided.
      • +
      • replace (bool, default True) – If True, an already existing corresponding pd.DataFrame +column in the case data component will be overwritten with a +new (empty) one.
      • +
      +
      Returns:

      DataSet is modified inplace, meta data and _data columns +will be added

      +
      Return type:

      None

      +
      + + +
      +
      +all(name, codes)
      +

      Return a logical has_all() slicer for the passed codes.

      +
      +

      Note

      +

      When applied to an array mask, the has_all() logic is ex- +tended to the item sources, i.e. the it must itself be true for +all the items.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str, default None) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • codes (int or list of int) – The codes to build the logical slicer from.
      • +
      +
      Returns:

      slicer – The indices fulfilling has_all([codes]).

      +
      Return type:

      pandas.Index

      +
      +
      + +
      +
      +any(name, codes)
      +

      Return a logical has_any() slicer for the passed codes.

      +
      +

      Note

      +

      When applied to an array mask, the has_any() logic is ex- +tended to the item sources, i.e. the it must itself be true for +at least one of the items.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str, default None) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • codes (int or list of int) – The codes to build the logical slicer from.
      • +
      +
      Returns:

      slicer – The indices fulfilling has_any([codes]).

      +
      Return type:

      pandas.Index

      +
      +
      + +
      +
      +band(name, bands, new_name=None, label=None, text_key=None)
      +

      Group numeric data with band definitions treated as group text labels.

      +

      Wrapper around derive() for quick banding of numeric +data.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] that will +be banded into summarized categories.
      • +
      • bands (list of int/tuple or dict mapping the former to value texts) – The categorical bands to be used. Bands can be single numeric +values or ranges, e.g.: [0, (1, 10), 11, 12, (13, 20)]. +Be default, each band will also make up the value text of the +category created in the _meta component. To specify custom +texts, map each band to a category name e.g.: +[{‘A’: 0}, +{‘B’: (1, 10)}, +{‘C’: 11}, +{‘D’: 12}, +{‘E’: (13, 20)}]
      • +
      • new_name (str, default None) – The created variable will be named '<name>_banded', unless a +desired name is provided explicitly here.
      • +
      • label (str, default None) – The created variable’s text label will be identical to the origi- +nating one’s passed in name, unless a desired label is provided +explicitly here.
      • +
      • text_key (str, default None) – Text key for text-based label information. Uses the +DataSet.text_key information if not provided.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +by_type(types=None)
      +

      Get an overview of all the variables ordered by their type.

      + +++ + + + + + + + +
      Parameters:types (str or list of str, default None) – Restrict the overview to these data types.
      Returns:overview – The variables per data type inside the DataSet.
      Return type:pandas.DataFrame
      +
      + +
      +
      +categorize(name, categorized_name=None)
      +

      Categorize an int/string/text variable to single.

      +

      The values object of the categorized variable is populated with the +unique values found in the originating variable (ignoring np.NaN / +empty row entries).

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'] that will +be categorized.
      • +
      • categorized_name (str) – If provided, the categorized variable’s new name will be drawn +from here, otherwise a default name in form of 'name#' will be +used.
      • +
      +
      Returns:

      DataSet is modified inplace, adding the categorized variable to it.

      +
      Return type:

      None

      +
      +
      + +
      +
      +clear_factors(name)
      +

      Remove all factors set in the variable’s 'values' object.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      Returns:
      Return type:None
      +
      + +
      +
      +clone()
      +

      Get a deep copy of the DataSet instance.

      +
      + +
      +
      +code_count(name, count_only=None, count_not=None)
      +

      Get the total number of codes/entries found per row.

      +
      +

      Note

      +

      Will be 0/1 for type single and range between 0 and the +number of possible values for type delimited set.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'] or +meta['masks'].
      • +
      • count_only (int or list of int, default None) – Pass a list of codes to restrict counting to.
      • +
      • count_not (int or list of int, default None) – Pass a list of codes that should no be counted.
      • +
      +
      Returns:

      count – A series with the results as ints.

      +
      Return type:

      pandas.Series

      +
      +
      + +
      +
      +code_from_label(name, text_label, text_key=None, exact=True, flat=True)
      +

      Return the code belonging to the passed text label (if present).

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • text_label (str or list of str) – The value text(s) to search for.
      • +
      • text_key (str, default None) – The desired text_key to search through. Uses the +DataSet.text_key information if not provided.
      • +
      • exact (bool, default True) – text_label must exactly match a categorical value’s text. +If False, it is enough that the category contains the text_label.
      • +
      • flat (If a list is passed for text_label, return all found codes) – as a regular list. If False, return a list of lists matching the order +of the text_label list.
      • +
      +
      Returns:

      codes – The list of value codes found for the passed label text.

      +
      Return type:

      list

      +
      +
      + +
      +
      +codes(name)
      +

      Get categorical data’s numerical code values.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in _meta['columns'].
      Returns:codes – The list of category codes.
      Return type:list
      +
      + +
      +
      +codes_in_data(name)
      +

      Get a list of codes that exist in data.

      +
      + +
      +
      +compare(dataset, variables=None, strict=False, text_key=None)
      +

      Compares types, codes, values, question labels of two datasets.

      + +++ + + + + + + + +
      Parameters:
        +
      • dataset (quantipy.DataSet instance) – Test if all variables in the provided dataset are also in +self and compare their metadata definitions.
      • +
      • variables (str, list of str) – Check only these variables
      • +
      • strict (bool, default False) – If True lower/ upper cases and spaces are taken into account.
      • +
      • text_key (str, list of str) – The textkeys for which texts are compared.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +convert(name, to)
      +

      Convert meta and case data between compatible variable types.

      +

      Wrapper around the separate as_TYPE() conversion methods.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'] that will +be converted.
      • +
      • to ({'int', 'float', 'single', 'delimited set', 'string'}) – The variable type to convert to.
      • +
      +
      Returns:

      The DataSet variable is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +copy(name, suffix='rec', copy_data=True, slicer=None, copy_only=None, copy_not=None)
      +

      Copy meta and case data of the variable defintion given per name.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • suffix (str, default 'rec') – The new variable name will be constructed by suffixing the original +name with _suffix, e.g. 'age_rec.
      • +
      • copy_data (bool, default True) – The new variable assumes the data of the original variable.
      • +
      • slicer (dict) – If the data is copied it is possible to filter the data with a +complex logic. Example: slicer = {‘q1’: not_any([99])}
      • +
      • copy_only (int or list of int, default None) – If provided, the copied version of the variable will only contain +(data and) meta for the specified codes.
      • +
      • copy_not (int or list of int, default None) – If provided, the copied version of the variable will contain +(data and) meta for the all codes, except of the indicated.
      • +
      +
      Returns:

      DataSet is modified inplace, adding a copy to both the data and meta +component.

      +
      Return type:

      None

      +
      +
      + +
      +
      +copy_array_data(source, target, source_items=None, target_items=None, slicer=None)
      +
      + +
      +
      +create_set(setname='new_set', based_on='data file', included=None, excluded=None, strings='keep', arrays='masks', replace=None, overwrite=False)
      +

      Create a new set in dataset._meta['sets'].

      + +++ + + + + + + + +
      Parameters:
        +
      • setname (str, default 'new_set') – Name of the new set.
      • +
      • based_on (str, default 'data file') – Name of set that can be reduced or expanded.
      • +
      • included (str or list/set/tuple of str) – Names of the variables to be included in the new set. If None all +variables in based_on are taken.
      • +
      • excluded (str or list/set/tuple of str) – Names of the variables to be excluded in the new set.
      • +
      • strings ({'keep', 'drop', 'only'}, default 'keep') – Keep, drop or only include string variables.
      • +
      • arrays ({'masks', 'columns'}, default masks) – For arrays add masks@varname or columns@varname.
      • +
      • replace (dict) – Replace a variable in the set with an other. +Example: {‘q1’: ‘q1_rec’}, ‘q1’ and ‘q1_rec’ must be included in +based_on. ‘q1’ will be removed and ‘q1_rec’ will be +moved to this position.
      • +
      • overwrite (bool, default False) – Overwrite if meta['sets'][name] already exist.
      • +
      +
      Returns:

      The DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +crosstab(x, y=None, w=None, pct=False, decimals=1, text=True, rules=False, xtotal=False, f=None)
      +
      + +
      +
      +cut_item_texts(arrays=None)
      +

      Remove array text from array item texts.

      + +++ + + + +
      Parameters:arrays (str, list of str, default None) – Cut texts for items of these arrays. If None, all keys in +._meta['masks'] are taken.
      +
      + +
      +
      +data()
      +

      Return the data component of the DataSet instance.

      +
      + +
      +
      +derive(name, qtype, label, cond_map, text_key=None)
      +

      Create meta and recode case data by specifying derived category logics.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'].
      • +
      • qtype ([int, float, single, delimited set]) – The structural type of the data the meta describes.
      • +
      • label (str) – The text label information.
      • +
      • cond_map (list of tuples) –

        Tuples of either two or three elements of following structures:

        +

        2 elements, no labels provided: +(code, <qp logic expression here>), e.g.: +(1, intersection([{'gender': [1]}, {'age': frange('30-40')}]))

        +

        2 elements, no codes provided: +(‘text label’, <qp logic expression here>), e.g.: +('Cat 1', intersection([{'gender': [1]}, {'age': frange('30-40')}]))

        +

        3 elements, with codes + labels: +(code, ‘Label goes here’, <qp logic expression here>), e.g.: +(1, 'Men, 30 to 40', intersection([{'gender': [1]}, {'age': frange('30-40')}]))

        +
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not provided.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +derotate(levels, mapper, other=None, unique_key='identity', dropna=True)
      +

      Derotate data and meta using the given mapper, and appending others.

      +

      This function derotates data using the specification defined in +mapper, which is a list of dicts of lists, describing how +columns from data can be read as a heirarchical structure.

      +

      Returns derotated DataSet instance and saves data and meta as json +and csv.

      + +++ + + + + + + + +
      Parameters:
        +
      • levels (dict) – The name and values of a new column variable to identify cases.
      • +
      • mapper (list of dicts of lists) –

        A list of dicts matching where the new column names are keys to +to lists of source columns. Example:

        +
        >>> mapper = [{'q14_1': ['q14_1_1', 'q14_1_2', 'q14_1_3']},
        +...           {'q14_2': ['q14_2_1', 'q14_2_2', 'q14_2_3']},
        +...           {'q14_3': ['q14_3_1', 'q14_3_2', 'q14_3_3']}]
        +
        +
        +
      • +
      • unique_key (str) – Name of column variable that will be copied to new dataset.
      • +
      • other (list (optional; default=None)) – A list of additional columns from the source data to be appended +to the end of the resulting stacked dataframe.
      • +
      • dropna (boolean (optional; default=True)) – Passed through to the pandas.DataFrame.stack() operation.
      • +
      +
      Returns:

      +
      Return type:

      new qp.DataSet instance

      +
      +
      + +
      +
      +describe(var=None, only_type=None, text_key=None, axis_edit=None)
      +

      Inspect the DataSet’s global or variable level structure.

      +
      + +
      +
      +dichotomize(name, value_texts=None, keep_variable_text=True, ignore=None, replace=False, text_key=None)
      +
      + +
      +
      +dimensionize(names=None)
      +

      Rename the dataset columns for Dimensions compatibility.

      +
      + +
      +
      +dimensionizing_mapper(names=None)
      +

      Return a renaming dataset mapper for dimensionizing names.

      + +++ + + + + + + + +
      Parameters:None
      Returns:mapper – A renaming mapper in the form of a dict of {old: new} that +maps non-Dimensions naming conventions to Dimensions naming +conventions.
      Return type:dict
      +
      + +
      +
      +drop(name, ignore_items=False)
      +

      Drops variables from meta and data components of the DataSet.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str or list of str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • ignore_items (bool) – If False source variables for arrays in _meta['columns'] +are dropped, otherwise kept.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +drop_duplicates(unique_id='identity', keep='first')
      +

      Drop duplicated cases from self._data.

      + +++ + + + +
      Parameters:
        +
      • unique_id (str) – Variable name that gets scanned for duplicates.
      • +
      • keep (str, {'first', 'last'}) – Keep first or last of the duplicates.
      • +
      +
      +
      + +
      +
      +duplicates(name='identity')
      +

      Returns a list with duplicated values for the provided name.

      + +++ + + + + + + + +
      Parameters:name (str, default 'identity') – The column variable name keyed in meta['columns'].
      Returns:vals – A list of duplicated values found in the named variable.
      Return type:list
      +
      + +
      +
      +empty(name, condition=None)
      +

      Check variables for emptiness (opt. restricted by a condition).

      + +++ + + + + + + + +
      Parameters:
        +
      • name ((list of) str) – The mask variable name keyed in _meta['columns'].
      • +
      • condition (Quantipy logic expression, default None) – A logical condition expressed as Quantipy logic that determines +which subset of the case data rows to be considered.
      • +
      +
      Returns:

      empty

      +
      Return type:

      bool

      +
      +
      + +
      +
      +empty_items(name, condition=None, by_name=True)
      +

      Test arrays for item emptiness (opt. restricted by a condition).

      + +++ + + + + + + + +
      Parameters:
        +
      • name ((list of) str) – The mask variable name keyed in _meta['masks'].
      • +
      • condition (Quantipy logic expression, default None) – A logical condition expressed as Quantipy logic that determines +which subset of the case data rows to be considered.
      • +
      • by_name (bool, default True) – Return array items by their name or their index.
      • +
      +
      Returns:

      empty – The list of empty items by their source names or positional index +(starting from 1!, mapped to their parent mask name if more than +one).

      +
      Return type:

      list

      +
      +
      + +
      +
      +extend_items(name, ext_items, text_key=None)
      +

      Extend mask items of an existing array.

      + +++ + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['masks'].
      • +
      • ext_items (list of str/ list of dict) – The label of the new item. It can be provided as str, then the new +column is named by the grid and the item_no, or as dict +{‘new_column’: ‘label’}.
      • +
      • text_key (str/ list of str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not provided.
      • +
      +
      +
      + +
      +
      +extend_values(name, ext_values, text_key=None, safe=True)
      +

      Add to the ‘values’ object of existing column or mask meta data.

      +

      Attempting to add already existing value codes or providing already +present value texts will both raise a ValueError!

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • ext_values (list of str or tuples in form of (int, str), default None) – When a list of str is given, the categorical values will simply be +enumerated and mapped to the category labels. Alternatively codes can +mapped to categorical labels, e.g.: +[(1, ‘Elephant’), (2, ‘Mouse’), (999, ‘No animal’)]
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not provided.
      • +
      • safe (bool, default True) – If set to False, duplicate value texts are allowed when extending +the values object.
      • +
      +
      Returns:

      The DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +factors(name)
      +

      Get categorical data’s stat. factor values.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      Returns:factors – A {value: factor} mapping.
      Return type:OrderedDict
      +
      + +
      +
      +filter(alias, condition, inplace=False)
      +

      Filter the DataSet using a Quantipy logical expression.

      +
      + +
      +
      +find(str_tags=None, suffixed=False)
      +

      Find variables by searching their names for substrings.

      + +++ + + + + + + + +
      Parameters:
        +
      • str_tags ((list of) str) – The strings tags to look for in the variable names. If not provided, +the modules’ default global list of substrings from VAR_SUFFIXES +will be used.
      • +
      • suffixed (bool, default False) – If set to True, only variable names that end with a given string +sequence will qualify.
      • +
      +
      Returns:

      found – The list of matching variable names.

      +
      Return type:

      list

      +
      +
      + +
      +
      +find_duplicate_texts(name, text_key=None)
      +

      Collect values that share the same text information to find duplicates.

      + +++ + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not +provided.
      • +
      +
      +
      + +
      +
      +flatten(name, codes, new_name=None, text_key=None)
      +

      Create a variable that groups array mask item answers to categories.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The array variable name keyed in meta['masks'] that will +be converted.
      • +
      • codes (int, list of int) – The answers codes that determine the categorical grouping. +Item labels will become the category labels.
      • +
      • new_name (str, default None) – The name of the new delimited set variable. If None, name is +suffixed with ‘_rec’.
      • +
      • text_key (str, default None) – Text key for text-based label information. Uses the +DataSet.text_key information if not provided.
      • +
      +
      Returns:

      The DataSet is modified inplace, delimited set variable is added.

      +
      Return type:

      None

      +
      +
      + +
      +
      +force_texts(copy_to=None, copy_from=None, update_existing=False)
      +

      Copy info from existing text_key to a new one or update the existing one.

      + +++ + + + + + + + +
      Parameters:
        +
      • copy_to (str) – {‘en-GB’, ‘da-DK’, ‘fi-FI’, ‘nb-NO’, ‘sv-SE’, ‘de-DE’} +None -> _meta[‘lib’][‘default text’] +The text key that will be filled.
      • +
      • copy_from (str / list) – {‘en-GB’, ‘da-DK’, ‘fi-FI’, ‘nb-NO’, ‘sv-SE’, ‘de-DE’} +You can also enter a list with text_keys, if the first text_key +doesn’t exist, it takes the next one
      • +
      • update_existing (bool) – True : copy_to will be filled in any case +False: copy_to will be filled if it’s empty/not existing
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +from_batch(batch_name, include='identity', text_key=[], apply_edits=True, additions='variables')
      +

      Get a filtered subset of the DataSet using qp.Batch definitions.

      + +++ + + + + + + + +
      Parameters:
        +
      • batch_name (str) – Name of a Batch included in the DataSet.
      • +
      • include (str/ list of str) – Name of variables that get included even if they are not in Batch.
      • +
      • text_key (str/ list of str, default None) – Take over all texts of the included text_key(s), if None is provided +all included text_keys are taken.
      • +
      • apply_edits (bool, default True) – meta_edits and rules are used as/ applied on global meta of the +new DataSet instance.
      • +
      • additions ({'variables', 'filters', 'full', None}) – Extend included variables by the xks, yks and weights of the +additional batches if set to ‘variables’, ‘filters’ will create +new 1/0-coded variables that reflect any filters defined. Selecting +‘full’ will do both, None will ignore additional Batches completely.
      • +
      +
      Returns:

      b_ds

      +
      Return type:

      quantipy.DataSet

      +
      +
      + +
      +
      +from_components(data_df, meta_dict=None, reset=True, text_key=None)
      +

      Attach data and meta directly to the DataSet instance.

      +
      +

      Note

      +

      Except testing for appropriate object types, this method +offers no additional safeguards or consistency/compability checks +with regard to the passed data and meta documents!

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • data_df (pandas.DataFrame) – A DataFrame that contains case data entries for the DataSet.
      • +
      • meta_dict (dict, default None) – A dict that stores meta data describing the columns of the data_df. +It is assumed to be well-formed following the Quantipy meta data +structure.
      • +
      • reset (bool, default True) – Clean the ‘lib’ and 'sets' metadata collections from non-native +entries, e.g. user-defined information or helper metadata.
      • +
      • text_key (str, default None) – The text_key to be used. If not provided, it will be attempted to +use the ‘default text’ from the meta['lib'] definition.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +from_excel(path_xlsx, merge=True, unique_key='identity')
      +

      Converts excel files to a dataset or/and merges variables.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_xlsx (str) – Path where the excel file is stored. The file must have exactly +one sheet with data.
      • +
      • merge (bool) – If True the new data from the excel file will be merged on the +dataset.
      • +
      • unique_key (str) – If merge=True an hmerge is done on this variable.
      • +
      +
      Returns:

      new_dataset – Contains only the data from excel. +If merge=True dataset is modified inplace.

      +
      Return type:

      quantipy.DataSet

      +
      +
      + +
      +
      +from_stack(stack, data_key=None, dk_filter=None, reset=True)
      +

      Use quantipy.Stack data and meta to create a DataSet instance.

      + +++ + + + + + + + +
      Parameters:
        +
      • stack (quantipy.Stack) – The Stack instance to convert.
      • +
      • data_key (str) – The reference name where meta and data information are stored.
      • +
      • dk_filter (string, default None) – Filter name if the stack contains more than one filters. If None +‘no_filter’ will be used.
      • +
      • reset (bool, default True) – Clean the ‘lib’ and 'sets' metadata collections from non-native +entries, e.g. user-defined information or helper metadata.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +fully_hidden_arrays()
      +

      Get all array definitions that contain only hidden items.

      + +++ + + + + + +
      Returns:hidden – The list of array mask names.
      Return type:list
      +
      + +
      +
      +get_batch(name)
      +

      Get existing Batch instance from DataSet meta information.

      + +++ + + + +
      Parameters:name (str) – Name of existing Batch instance.
      +
      + +
      +
      +get_property(name, prop_name, text_key=None)
      +
      + +
      +
      +hide_empty_items(condition=None, arrays=None)
      +

      Apply rules meta to automatically hide empty array items.

      + +++ + + + + + + + +
      Parameters:
        +
      • name ((list of) str, default None) – The array mask variable names keyed in _meta['masks']. If not +explicitly provided will test all array mask definitions.
      • +
      • condition (Quantipy logic expression) – A logical condition expressed as Quantipy logic that determines +which subset of the case data rows to be considered.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +hiding(name, hide, axis='y', hide_values=True)
      +

      Set or update rules[axis]['dropx'] meta for the named column.

      +

      Quantipy builds will respect the hidden codes and cut them from +results.

      +
      +

      Note

      +

      This is not equivalent to DataSet.set_missings() as +missing values are respected also in computations.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str or list of str) – The column variable(s) name keyed in _meta['columns'].
      • +
      • hide (int or list of int) – Values indicated by their int codes will be dropped from +Quantipy.View.dataframes.
      • +
      • axis ({'x', 'y'}, default 'y') – The axis to drop the values from.
      • +
      • hide_values (bool, default True) – Only considered if name refers to a mask. If True, values are +hidden on all mask items. If False, mask items are hidden by position +(only for array summaries).
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +hmerge(dataset, on=None, left_on=None, right_on=None, overwrite_text=False, from_set=None, inplace=True, merge_existing=None, verbose=True)
      +

      Merge Quantipy datasets together using an index-wise identifer.

      +

      This function merges two Quantipy datasets together, updating variables +that exist in the left dataset and appending others. New variables +will be appended in the order indicated by the ‘data file’ set if +found, otherwise they will be appended in alphanumeric order. +This merge happend horizontally (column-wise). Packed kwargs will be +passed on to the pandas.DataFrame.merge() method call, but that merge +will always happen using how=’left’.

      + +++ + + + + + + + +
      Parameters:
        +
      • dataset (quantipy.DataSet) – The dataset to merge into the current DataSet.
      • +
      • on (str, default=None) – The column to use as a join key for both datasets.
      • +
      • left_on (str, default=None) – The column to use as a join key for the left dataset.
      • +
      • right_on (str, default=None) – The column to use as a join key for the right dataset.
      • +
      • overwrite_text (bool, default=False) – If True, text_keys in the left meta that also exist in right +meta will be overwritten instead of ignored.
      • +
      • from_set (str, default=None) – Use a set defined in the right meta to control which columns are +merged from the right dataset.
      • +
      • inplace (bool, default True) – If True, the DataSet will be modified inplace with new/updated +columns. Will return a new DataSet instance if False.
      • +
      • verbose (bool, default=True) – Echo progress feedback to the output pane.
      • +
      +
      Returns:

      None or new_dataset – If the merge is not applied inplace, a DataSet instance +is returned.

      +
      Return type:

      quantipy.DataSet

      +
      +
      + +
      +
      +interlock(name, label, variables, val_text_sep='/')
      +

      Build a new category-intersected variable from >=2 incoming variables.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The new column variable name keyed in _meta['columns'].
      • +
      • label (str) – The new text label for the created variable.
      • +
      • variables (list of >= 2 str or dict (mapper)) –

        The column names of the variables that are feeding into the +intersecting recode operation. Or dicts/mapper to create temporary +variables for interlock. Can also be a mix of str and dict. Example:

        +
        >>> ['gender',
        +...  {'agegrp': [(1, '18-34', {'age': frange('18-34')}),
        +...              (2, '35-54', {'age': frange('35-54')}),
        +...              (3, '55+', {'age': is_ge(55)})]},
        +...  'region']
        +
        +
        +
      • +
      • val_text_sep (str, default '/') – The passed character (or any other str value) wil be used to +separate the incoming individual value texts to make up the inter- +sected category value texts, e.g.: ‘Female/18-30/London’.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +is_like_numeric(name)
      +

      Test if a string-typed variable can be expressed numerically.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in _meta['columns'].
      Returns:
      Return type:bool
      +
      + +
      +
      +is_nan(name)
      +

      Detect empty entries in the _data rows.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in meta['columns'].
      Returns:count – A series with the results as bool.
      Return type:pandas.Series
      +
      + +
      +
      +item_no(name)
      +

      Return the order/position number of passed array item variable name.

      + +++ + + + + + + + +
      Parameters:name (str) – The column variable name keyed in _meta['columns'].
      Returns:no – The positional index of the item (starting from 1).
      Return type:int
      +
      + +
      +
      +item_texts(name, text_key=None, axis_edit=None)
      +

      Get the text meta data for the items of the passed array mask name.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The mask variable name keyed in _meta['masks'].
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      texts – The list of item texts for the array elements.

      +
      Return type:

      list

      +
      +
      + +
      +
      +items(name, text_key=None, axis_edit=None)
      +

      Get the array’s paired item names and texts information from the meta.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['masks'].
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      items – The list of source item names (from _meta['columns']) and their +text information packed as tuples.

      +
      Return type:

      list of tuples

      +
      +
      + +
      + +

      Create a Link instance from the DataSet.

      +
      + +
      +
      +merge_texts(dataset)
      +

      Add additional text versions from other text_key meta.

      +

      Case data will be ignored during the merging process.

      + +++ + + + + + + + +
      Parameters:dataset ((A list of multiple) quantipy.DataSet) – One or multiple datasets that provide new text_key meta.
      Returns:
      Return type:None
      +
      + +
      +
      +meta(name=None, text_key=None, axis_edit=None)
      +

      Provide a pretty summary for variable meta given as per name.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str, default None) – The variable name keyed in _meta['columns'] or _meta['masks']. +If None, the entire meta component of the DataSet instance +will be returned.
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      meta – Either a DataFrame that sums up the meta information on a mask +or column or the meta dict as a whole is

      +
      Return type:

      dict or pandas.DataFrame

      +
      +
      + +
      +
      +meta_to_json(key=None, collection=None)
      +

      Save a meta object as json file.

      + +++ + + + + + + + +
      Parameters:
        +
      • key (str, default None) – Name of the variable whose metadata is saved, if key is not +provided included collection or the whole meta is saved.
      • +
      • collection (str {'columns', 'masks', 'sets', 'lib'}, default None) – The meta object is taken from this collection.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +min_value_count(name, min=50, weight=None, condition=None, axis='y', verbose=True)
      +

      Wrapper for self.hiding(), which is hiding low value_counts.

      + +++ + + + +
      Parameters:
        +
      • variables (str/ list of str) – Name(s) of the variable(s) whose values are checked against the +defined border.
      • +
      • min (int) – If the amount of counts for a value is below this number, the +value is hidden.
      • +
      • weight (str, default None) – Name of the weight, which is used to calculate the weigthed counts.
      • +
      • condition (complex logic) – The data, which is used to calculate the counts, can be filtered +by the included condition.
      • +
      • axis ({'y', 'x', ['x', 'y']}, default None) – The axis on which the values are hidden.
      • +
      +
      +
      + +
      +
      +names(ignore_items=True)
      +

      Find all weak-duplicate variable names that are different only by case.

      +
      +

      Note

      +

      Will return self.variables() if no weak-duplicates are found.

      +
      + +++ + + + + + +
      Returns:weak_dupes – An overview of case-sensitive spelling differences in otherwise +equal variable names.
      Return type:pd.DataFrame
      +
      + +
      +
      +order(new_order=None, reposition=None, regroup=False)
      +

      Set the global order of the DataSet variables collection.

      +

      The global order of the DataSet is reflected in the data component’s +pd.DataFrame.columns order and the variable references in the meta +component’s ‘data file’ items.

      + +++ + + + + + + + +
      Parameters:
        +
      • new_order (list) – A list of all DataSet variables in the desired order.
      • +
      • reposition ((List of) dict) – Each dict maps one or a list of variables to a reference variable +name key. The mapped variables are moved before the reference key.
      • +
      • regroup (bool, default False) – Attempt to regroup non-native variables (i.e. created either +manually with add_meta(), recode(), derive(), etc. +or automatically by manifesting qp.View objects) with their +originating variables.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +parents(name)
      +

      Get the parent meta information for masks-structured column elements.

      + +++ + + + + + + + +
      Parameters:name (str) – The mask variable name keyed in _meta['columns'].
      Returns:parents – The list of parents the _meta['columns'] variable is attached to.
      Return type:list
      +
      + +
      +
      +populate(batches='all', verbose=True)
      +

      Create a qp.Stack based on all available qp.Batch definitions.

      + +++ + + + + + + + +
      Parameters:batches (str/ list of str) – Name(s) of qp.Batch instances that are used to populate the +qp.Stack.
      Returns:
      Return type:qp.Stack
      +
      + +
      +
      +read_ascribe(path_meta, path_data, text_key)
      +

      Load Dimensions .xml/.txt files, connecting as data and meta components.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_meta (str) – The full path (optionally with extension '.xml', otherwise +assumed as such) to the meta data defining '.xml' file.
      • +
      • path_data (str) – The full path (optionally with extension '.txt', otherwise +assumed as such) to the case data defining '.txt' file.
      • +
      +
      Returns:

      The DataSet is modified inplace, connected to Quantipy data +and meta components that have been converted from their Ascribe +source files.

      +
      Return type:

      None

      +
      +
      + +
      +
      +read_dimensions(path_meta, path_data)
      +

      Load Dimensions .ddf/.mdd files, connecting as data and meta components.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_meta (str) – The full path (optionally with extension '.mdd', otherwise +assumed as such) to the meta data defining '.mdd' file.
      • +
      • path_data (str) – The full path (optionally with extension '.ddf', otherwise +assumed as such) to the case data defining '.ddf' file.
      • +
      +
      Returns:

      The DataSet is modified inplace, connected to Quantipy data +and meta components that have been converted from their Dimensions +source files.

      +
      Return type:

      None

      +
      +
      + +
      +
      +read_quantipy(path_meta, path_data, reset=True)
      +

      Load Quantipy .csv/.json files, connecting as data and meta components.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_meta (str) – The full path (optionally with extension '.json', otherwise +assumed as such) to the meta data defining '.json' file.
      • +
      • path_data (str) – The full path (optionally with extension '.csv', otherwise +assumed as such) to the case data defining '.csv' file.
      • +
      • reset (bool, default True) – Clean the ‘lib’ and 'sets' metadata collections from non-native +entries, e.g. user-defined information or helper metadata.
      • +
      +
      Returns:

      The DataSet is modified inplace, connected to Quantipy native +data and meta components.

      +
      Return type:

      None

      +
      +
      + +
      +
      +read_spss(path_sav, **kwargs)
      +

      Load SPSS Statistics .sav files, converting and connecting data/meta.

      + +++ + + + + + + + +
      Parameters:path_sav (str) – The full path (optionally with extension '.sav', otherwise +assumed as such) to the '.sav' file.
      Returns:The DataSet is modified inplace, connected to Quantipy data +and meta components that have been converted from the SPSS +source file.
      Return type:None
      +
      + +
      +
      +recode(target, mapper, default=None, append=False, intersect=None, initialize=None, fillna=None, inplace=True)
      +

      Create a new or copied series from data, recoded using a mapper.

      +

      This function takes a mapper of {key: logic} entries and injects the +key into the target column where its paired logic is True. The logic +may be arbitrarily complex and may refer to any other variable or +variables in data. Where a pre-existing column has been used to +start the recode, the injected values can replace or be appended to +any data found there to begin with. Note that this function does +not edit the target column, it returns a recoded copy of the target +column. The recoded data will always comply with the column type +indicated for the target column according to the meta.

      + +++ + + + + + + + +
      Parameters:
        +
      • target (str) – The column variable name keyed in _meta['columns'] that is the +target of the recode. If not found in _meta this will fail +with an error. If target is not found in data.columns the +recode will start from an empty series with the same index as +_data. If target is found in data.columns the recode will +start from a copy of that column.
      • +
      • mapper (dict) – A mapper of {key: logic} entries.
      • +
      • default (str, default None) – The column name to default to in cases where unattended lists +are given in your logic, where an auto-transformation of +{key: list} to {key: {default: list}} is provided. Note that +lists in logical statements are themselves a form of shorthand +and this will ultimately be interpreted as: +{key: {default: has_any(list)}}.
      • +
      • append (bool, default False) – Should the new recoded data be appended to values already found +in the series? If False, data from series (where found) will +overwrite whatever was found for that item instead.
      • +
      • intersect (logical statement, default None) – If a logical statement is given here then it will be used as an +implied intersection of all logical conditions given in the +mapper.
      • +
      • initialize (str or np.NaN, default None) – If not None, a copy of the data named column will be used to +populate the target column before the recode is performed. +Alternatively, initialize can be used to populate the target +column with np.NaNs (overwriting whatever may be there) prior +to the recode.
      • +
      • fillna (int, default=None) – If not None, the value passed to fillna will be used on the +recoded series as per pandas.Series.fillna().
      • +
      • inplace (bool, default True) – If True, the DataSet will be modified inplace with new/updated +columns. Will return a new recoded pandas.Series instance if +False.
      • +
      +
      Returns:

      Either the DataSet._data is modified inplace or a new +pandas.Series is returned.

      +
      Return type:

      None or recode_series

      +
      +
      + +
      +
      +remove_html()
      +

      Cycle through all meta text objects removing html tags.

      +

      Currently uses the regular expression ‘<.*?>’ in _remove_html() +classmethod.

      + +++ + + + + + +
      Returns:
      Return type:None
      +
      + +
      +
      +remove_items(name, remove)
      +

      Erase array mask items safely from both meta and case data components.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['masks'].
      • +
      • remove (int or list of int) – The items listed by their order number in the +_meta['masks'][name]['items'] object will be droped from the +mask definition.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +remove_values(name, remove)
      +

      Erase value codes safely from both meta and case data components.

      +

      Attempting to remove all value codes from the variable’s value object +will raise a ValueError!

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • remove (int or list of int) – The codes to be removed from the DataSet variable.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +rename(name, new_name)
      +

      Change meta and data column name references of the variable defintion.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • new_name (str) – The new variable name.
      • +
      +
      Returns:

      DataSet is modified inplace. The new name reference replaces the +original one.

      +
      Return type:

      None

      +
      +
      + +
      +
      +rename_from_mapper(mapper, keep_original=False)
      +

      Rename meta objects and data columns using mapper.

      + +++ + + + + + + + +
      Parameters:mapper (dict) – A renaming mapper in the form of a dict of {old: new} that +will be used to rename columns throughout the meta and data.
      Returns:DataSet is modified inplace.
      Return type:None
      +
      + +
      +
      +reorder_items(name, new_order)
      +

      Apply a new order to mask items.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The variable name keyed in _meta['masks'].
      • +
      • new_order (list of int, default None) – The new order of the mask items. The included ints match up to +the number of the items (DataSet.item_no('item_name')).
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +reorder_values(name, new_order=None)
      +

      Apply a new order to the value codes defined by the meta data component.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • new_order (list of int, default None) – The new code order of the DataSet variable. If no order is given, +the values object is sorted ascending.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +repair()
      +

      Try to fix legacy meta data inconsistencies and badly shaped array / +datafile items 'sets' meta definitions.

      +
      + +
      +
      +repair_text_edits(text_key=None)
      +

      Cycle through all meta text objects repairing axis edits.

      + +++ + + + + + + + +
      Parameters:text_key (str / list of str, default None) – {None, ‘en-GB’, ‘da-DK’, ‘fi-FI’, ‘nb-NO’, ‘sv-SE’, ‘de-DE’} +The text_keys for which text edits should be included.
      Returns:
      Return type:None
      +
      + +
      +
      +replace_texts(replace, text_key=None)
      +

      Cycle through all meta text objects replacing unwanted strings.

      + +++ + + + + + + + +
      Parameters:
        +
      • replace (dict, default Nonea) – A dictionary mapping {unwanted string: replacement string}.
      • +
      • text_key (str / list of str, default None) – {None, ‘en-GB’, ‘da-DK’, ‘fi-FI’, ‘nb-NO’, ‘sv-SE’, ‘de-DE’} +The text_keys for which unwanted strings are replaced.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +resolve_name(name)
      +
      + +
      +
      +restore_item_texts(arrays=None)
      +

      Restore array item texts.

      + +++ + + + +
      Parameters:arrays (str, list of str, default None) – Restore texts for items of these arrays. If None, all keys in +._meta['masks'] are taken.
      +
      + +
      +
      +revert()
      +

      Return to a previously saved state of the DataSet.

      +
      +

      Note

      +

      This method is designed primarily for use in interactive +Python environments like iPython/Jupyter and their notebook +applications.

      +
      +
      + +
      +
      +roll_up(varlist, ignore_arrays=None)
      +

      Replace any array items with their parent mask variable definition name.

      + +++ + + + + + + + +
      Parameters:
        +
      • varlist (list) – A list of meta 'columns' and/or 'masks' names.
      • +
      • ignore_arrays ((list of) str) – A list of array mask names that should not be rolled up if their +items are found inside varlist.
      • +
      +
      Returns:

      rolled_up – The modified varlist.

      +
      Return type:

      list

      +
      +
      + +
      +
      +save()
      +

      Save the current state of the DataSet’s data and meta.

      +

      The saved file will be temporarily stored inside the cache. Use this +to take a snapshot of the DataSet state to easily revert back to at a +later stage.

      +
      +

      Note

      +

      This method is designed primarily for use in interactive +Python environments like iPython/Jupyter notebook applications.

      +
      +
      + +
      +
      +select_text_keys(text_key=None)
      +

      Cycle through all meta text objects keep only selected text_key.

      + +++ + + + + + + + +
      Parameters:text_key (str / list of str, default None) – {None, ‘en-GB’, ‘da-DK’, ‘fi-FI’, ‘nb-NO’, ‘sv-SE’, ‘de-DE’} +The text_keys which should be kept.
      Returns:
      Return type:None
      +
      + +
      +
      +classmethod set_encoding(encoding)
      +

      Hack sys.setdefaultencoding() to escape ASCII hell.

      + +++ + + + +
      Parameters:encoding (str) – The name of the encoding to default to.
      +
      + +
      +
      +set_factors(name, factormap, safe=False)
      +

      Apply numerical factors to (single-type categorical) variables.

      +

      Factors can be read while aggregating descrp. stat. qp.Views.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • factormap (dict) – A mapping of {value: factor} (int to int).
      • +
      • safe (bool, default False) – Set to True to prevent setting factors to the values meta +data of non-single type variables.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_item_texts(name, renamed_items, text_key=None, axis_edit=None)
      +

      Rename or add item texts in the items objects of masks.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['masks'].
      • +
      • renamed_items (dict) –

        A dict mapping with following structure (array mask items are +assumed to be passed by their order number):

        +
        >>> {1: 'new label for item #1',
        +...  5: 'new label for item #5'}
        +
        +
        +
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not +provided.
      • +
      • axis_edit ({'x', 'y', ['x', 'y']}, default None) – If the new_text of the variable should only be considered temp. +for build exports, the axes on that the edited text should appear +can be provided.
      • +
      +
      Returns:

      The DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_missings(var, missing_map='default', hide_on_y=True, ignore=None)
      +

      Flag category definitions for exclusion in aggregations.

      + +++ + + + + + + + +
      Parameters:
        +
      • var (str or list of str) – Variable(s) to apply the meta flags to.
      • +
      • missing_map ('default' or list of codes or dict of {'flag': code(s)}, default 'default') – A mapping of codes to flags that can either be ‘exclude’ (globally +ignored) or ‘d.exclude’ (only ignored in descriptive statistics). +Codes provided in a list are flagged as ‘exclude’. +Passing ‘default’ is using a preset list of (TODO: specify) values +for exclusion.
      • +
      • ignore (str or list of str, default None) – A list of variables that should be ignored when applying missing +flags via the ‘default’ list method.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_property(name, prop_name, prop_value, ignore_items=False)
      +

      Access and set the value of a meta object’s properties collection.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • prop_name (str) – The property key name.
      • +
      • prop_value (any) – The value to be set for the property. Must be of valid type and +have allowed values(s) with regard to the property.
      • +
      • ignore_items (bool, default False) – When name refers to a variable from the 'masks' collection, +setting to True will ignore any items and only apply the +property to the mask itself.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_text_key(text_key)
      +

      Set the default text_key of the DataSet.

      +
      +

      Note

      +

      A lot of the instance methods will fall back to the default +text key in _meta['lib']['default text']. It is therefore +important to use this method with caution, i.e. ensure that the +meta contains text entries for the text_key set.

      +
      + +++ + + + + + + + +
      Parameters:text_key ({'en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE'}) – The text key that will be set in _meta['lib']['default text'].
      Returns:
      Return type:None
      +
      + +
      +
      +set_value_texts(name, renamed_vals, text_key=None, axis_edit=None)
      +

      Rename or add value texts in the ‘values’ object.

      +

      This method works for array masks and column meta data.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • renamed_vals (dict) – A dict mapping with following structure: +{1: 'new label for code=1', 5: 'new label for code=5'} +Codes will be ignored if they do not exist in the ‘values’ object.
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not +provided.
      • +
      • axis_edit ({'x', 'y', ['x', 'y']}, default None) – If renamed_vals should only be considered temp. for build +exports, the axes on that the edited text should appear can be +provided.
      • +
      +
      Returns:

      The DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_variable_text(name, new_text, text_key=None, axis_edit=None)
      +

      Apply a new or update a column’s/masks’ meta text object.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating column variable name keyed in meta['columns'] +or meta['masks'].
      • +
      • new_text (str) – The text (label) to be set.
      • +
      • text_key (str, default None) – Text key for text-based label information. Will automatically fall +back to the instance’s text_key property information if not provided.
      • +
      • axis_edit ({'x', 'y', ['x', 'y']}, default None) – If the new_text of the variable should only be considered temp. +for build exports, the axes on that the edited text should appear +can be provided.
      • +
      +
      Returns:

      The DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +set_verbose_errmsg(verbose=True)
      +
      + +
      +
      +set_verbose_infomsg(verbose=True)
      +
      + +
      +
      +slicing(name, slicer, axis='y')
      +

      Set or update rules[axis]['slicex'] meta for the named column.

      +

      Quantipy builds will respect the kept codes and show them exclusively +in results.

      +
      +

      Note

      +

      This is not a replacement for DataSet.set_missings() as +missing values are respected also in computations.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str or list of str) – The column variable(s) name keyed in _meta['columns'].
      • +
      • slice (int or list of int) – Values indicated by their int codes will be shown in +Quantipy.View.dataframes, respecting the provided order.
      • +
      • axis ({'x', 'y'}, default 'y') – The axis to slice the values on.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +sorting(name, on='@', within=False, between=False, fix=None, ascending=False, sort_by_weight=None)
      +

      Set or update rules['x']['sortx'] meta for the named column.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str or list of str) – The column variable(s) name keyed in _meta['columns'].
      • +
      • within (bool, default True) – Applies only to variables that have been aggregated by creating a +an expand grouping / overcode-style View: +If True, will sort frequencies inside each group.
      • +
      • between (bool, default True) – Applies only to variables that have been aggregated by creating a +an expand grouping / overcode-style View: +If True, will sort group and regular code frequencies with regard +to each other.
      • +
      • fix (int or list of int, default None) – Values indicated by their int codes will be ignored in +the sorting operation.
      • +
      • ascending (bool, default False) – By default frequencies are sorted in descending order. Specify +True to sort ascending.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +sources(name)
      +

      Get the _meta['columns'] elements for the passed array mask name.

      + +++ + + + + + + + +
      Parameters:name (str) – The mask variable name keyed in _meta['masks'].
      Returns:sources – The list of source elements from the array definition.
      Return type:list
      +
      + +
      +
      +split(save=False)
      +

      Return the meta and data components of the DataSet instance.

      + +++ + + + + + + + +
      Parameters:save (bool, default False) – If True, the meta and data objects will be saved to disk, +using the instance’s name and path attributes to determine +the file location.
      Returns:meta, data – The meta dict and the case data DataFrame as separate objects.
      Return type:dict, pandas.DataFrame
      +
      + +
      +
      +static start_meta(text_key='main')
      +

      Starts a new/empty Quantipy meta document.

      + +++ + + + + + + + +
      Parameters:text_key (str, default None) – The default text key to be set into the new meta document.
      Returns:meta – Quantipy meta object
      Return type:dict
      +
      + +
      +
      +subset(variables=None, from_set=None, inplace=False)
      +

      Create a cloned version of self with a reduced collection of variables.

      + +++ + + + + + + + +
      Parameters:
        +
      • variables (str or list of str, default None) – A list of variable names to include in the new DataSet instance.
      • +
      • from_set (str) – The name of an already existing set to base the new DataSet on.
      • +
      +
      Returns:

      subset_ds – The new reduced version of the DataSet.

      +
      Return type:

      qp.DataSet

      +
      +
      + +
      +
      +take(condition)
      +

      Create an index slicer to select rows from the DataFrame component.

      + +++ + + + + + + + +
      Parameters:condition (Quantipy logic expression) – A logical condition expressed as Quantipy logic that determines +which subset of the case data rows to be kept.
      Returns:slicer – The indices fulfilling the passed logical condition.
      Return type:pandas.Index
      +
      + +
      +
      +text(name, shorten=True, text_key=None, axis_edit=None)
      +

      Return the variables text label information.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str, default None) – The variable name keyed in _meta['columns'] or _meta['masks'].
      • +
      • shorten (bool, default True) – If True, text label meta from array items will not report +the parent mask’s text. Setting it to False will show the +“full” label.
      • +
      • text_key (str, default None) – The default text key to be set into the new meta document.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      text – The text metadata.

      +
      Return type:

      str

      +
      +
      + +
      +
      +to_array(name, variables, label, safe=True)
      +

      Combines column variables with same values meta into an array.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – Name of new grid.
      • +
      • variables (list of str or list of dicts) – Variable names that become items of the array. New item labels can +be added as dict. Example: +variables = [‘q1_1’, {‘q1_2’: ‘shop 2’}, {‘q1_3’: ‘shop 3’}]
      • +
      • label (str) – Text label for the mask itself.
      • +
      • safe (bool, default True) – If True, the method will raise a ValueError if the provided +variable name is already present in self. Select False to +forcefully overwrite an existing variable with the same name +(independent of its type).
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +to_delimited_set(name, label, variables, from_dichotomous=True, codes_from_name=True)
      +

      Combines multiple single variables to new delimited set variable.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – Name of new delimited set
      • +
      • label (str) – Label text for the new delimited set.
      • +
      • variables (list of str or list of tuples) – variables that get combined into the new delimited set. If they are +dichotomous (from_dichotomous=True), the labels of the variables +are used as category texts or if tuples are included, the second +items will be used for the category texts. +If the variables are categorical (from_dichotomous=False) the values +of the variables need to be eqaul and are taken for the delimited set.
      • +
      • from_dichotomous (bool, default True) – Define if the input variables are dichotomous or categorical.
      • +
      • codes_from_name (bool, default True) – If from_dichotomous=True, the codes can be taken from the Variable +names, if they are in form of ‘q01_1’, ‘q01_3’, … +In this case the codes will be 1, 3, ….
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +transpose(name, new_name=None, ignore_items=None, ignore_values=None, copy_data=True, text_key=None)
      +

      Create a new array mask with transposed items / values structure.

      +

      This method will automatically create meta and case data additions in +the DataSet instance.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The originating mask variable name keyed in meta['masks'].
      • +
      • new_name (str, default None) – The name of the new mask. If not provided explicitly, the new_name +will be constructed constructed by suffixing the original +name with ‘_trans’, e.g. 'Q2Array_trans.
      • +
      • ignore_items (int or list of int, default None) – If provided, the items listed by their order number in the +_meta['masks'][name]['items'] object will not be part of the +transposed array. This means they will be ignored while creating +the new value codes meta.
      • +
      • ignore_codes (int or list of int, default None) – If provided, the listed code values will not be part of the +transposed array. This means they will not be part of the new +item meta.
      • +
      • text_key (str) – The text key to be used when generating text objects, i.e. +item and value labels.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +unbind(name)
      +

      Remove mask-structure for arrays

      +
      + +
      +
      +uncode(target, mapper, default=None, intersect=None, inplace=True)
      +

      Create a new or copied series from data, recoded using a mapper.

      + +++ + + + + + + + +
      Parameters:
        +
      • target (str) – The variable name that is the target of the uncode. If it is keyed +in _meta['masks'] the uncode is done for all mask items. +If not found in _meta this will fail with an error.
      • +
      • mapper (dict) – A mapper of {key: logic} entries.
      • +
      • default (str, default None) – The column name to default to in cases where unattended lists +are given in your logic, where an auto-transformation of +{key: list} to {key: {default: list}} is provided. Note that +lists in logical statements are themselves a form of shorthand +and this will ultimately be interpreted as: +{key: {default: has_any(list)}}.
      • +
      • intersect (logical statement, default None) – If a logical statement is given here then it will be used as an +implied intersection of all logical conditions given in the +mapper.
      • +
      • inplace (bool, default True) – If True, the DataSet will be modified inplace with new/updated +columns. Will return a new recoded pandas.Series instance if +False.
      • +
      +
      Returns:

      Either the DataSet._data is modified inplace or a new +pandas.Series is returned.

      +
      Return type:

      None or uncode_series

      +
      +
      + +
      +
      +undimensionize(names=None, mapper_to_meta=False)
      +

      Rename the dataset columns to remove Dimensions compatibility.

      +
      + +
      +
      +undimensionizing_mapper(names=None)
      +

      Return a renaming dataset mapper for un-dimensionizing names.

      + +++ + + + + + + + +
      Parameters:None
      Returns:mapper – A renaming mapper in the form of a dict of {old: new} that +maps Dimensions naming conventions to non-Dimensions naming +conventions.
      Return type:dict
      +
      + +
      +
      +unify_values(name, code_map, slicer=None, exclusive=False)
      +

      Use a mapping of old to new codes to replace code values in _data.

      +
      +

      Note

      +

      Experimental! Check results carefully!

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in meta['columns'].
      • +
      • code_map (dict) – A mapping of {old: new}; old and new must be the +int-type code values from the column meta data.
      • +
      • slicer (Quantipy logic statement, default None) – If provided, the values will only be unified for cases where the +condition holds.
      • +
      • exclusive (bool, default False) – If True, the recoded unified value will replace whatever is already +found in the _data column, ignoring delimited set typed data +to which normally would get appended to.
      • +
      +
      Returns:

      +
      Return type:

      None

      +
      +
      + +
      +
      +unroll(varlist, keep=None, both=None)
      +

      Replace mask with their items, optionally excluding/keeping certain ones.

      + +++ + + + + + + + +
      Parameters:
        +
      • varlist (list) – A list of meta 'columns' and/or 'masks' names.
      • +
      • keep (str or list, default None) – The names of masks that will not be replaced with their items.
      • +
      • both ('all', str or list of str, default None) – The names of masks that will be included both as themselves and as +collections of their items.
      • +
      +
      Returns:

      unrolled – The modified varlist.

      +
      Return type:

      list

      +
      +
      + +
      +
      +update(data, on='identity')
      +

      Update the DataSet with the case data entries found in data.

      + +++ + + + + + + + +
      Parameters:
        +
      • data (pandas.DataFrame) – A dataframe that contains a subset of columns from the DataSet +case data component.
      • +
      • on (str, default 'identity') – The column to use as a join key.
      • +
      +
      Returns:

      DataSet is modified inplace.

      +
      Return type:

      None

      +
      +
      + +
      +
      +validate(spss_limits=False, verbose=True)
      +

      Identify and report inconsistencies in the DataSet instance.

      +
      +
      name:
      +
      column/mask name and meta[collection][var]['name'] are not identical
      +
      q_label:
      +
      text object is badly formatted or has empty text mapping
      +
      values:
      +
      categorical variable does not contain values, value text is badly +formatted or has empty text mapping
      +
      text_keys:
      +
      dataset.text_key is not included or existing text keys are not +consistent (also for parents)
      +
      source:
      +
      parents or items do not exist
      +
      codes:
      +
      codes in data component are not included in meta component
      +
      spss limit name:
      +
      length of name is greater than spss limit (64 characters) +(only shown if spss_limits=True)
      +
      spss limit q_label:
      +
      length of q_label is greater than spss limit (256 characters) +(only shown if spss_limits=True)
      +
      spss limit values:
      +
      length of any value text is greater than spss limit (120 characters) +(only shown if spss_limits=True)
      +
      +
      + +
      +
      +value_texts(name, text_key=None, axis_edit=None)
      +

      Get categorical data’s text information.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'].
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      texts – The list of category texts.

      +
      Return type:

      list

      +
      +
      + +
      +
      +values(name, text_key=None, axis_edit=None)
      +

      Get categorical data’s paired code and texts information from the meta.

      + +++ + + + + + + + +
      Parameters:
        +
      • name (str) – The column variable name keyed in _meta['columns'] or +_meta['masks'].
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta.
      • +
      • axis_edit ({'x', 'y'}, default None) – If provided the text_key is taken from the x/y edits dict.
      • +
      +
      Returns:

      values – The list of the numerical category codes and their texts +packed as tuples.

      +
      Return type:

      list of tuples

      +
      +
      + +
      +
      +variables(setname='data file', numeric=True, string=True, date=True, boolean=True, blacklist=None)
      +

      View all DataSet variables listed in their global order.

      + +++ + + + + + + + +
      Parameters:
        +
      • setname (str, default 'data file') – The name of the variable set to query. Defaults to the main +variable collection stored via ‘data file’.
      • +
      • numeric (bool, default True) – Include int and float type variables?
      • +
      • string (bool, default True) – Include string type variables?
      • +
      • date (bool, default True) – Include date type variables?
      • +
      • boolean (bool, default True) – Include boolean type variables?
      • +
      • blacklist (list, default None) – A list of variables names to exclude from the variable listing.
      • +
      +
      Returns:

      varlist – The list of variables registered in the queried set.

      +
      Return type:

      list

      +
      +
      + +
      +
      +vmerge(dataset, on=None, left_on=None, right_on=None, row_id_name=None, left_id=None, right_id=None, row_ids=None, overwrite_text=False, from_set=None, uniquify_key=None, reset_index=True, inplace=True, verbose=True)
      +

      Merge Quantipy datasets together by appending rows.

      +

      This function merges two Quantipy datasets together, updating variables +that exist in the left dataset and appending others. New variables +will be appended in the order indicated by the ‘data file’ set if +found, otherwise they will be appended in alphanumeric order. This +merge happens vertically (row-wise).

      + +++ + + + + + + + +
      Parameters:
        +
      • dataset ((A list of multiple) quantipy.DataSet) – One or multiple datasets to merge into the current DataSet.
      • +
      • on (str, default=None) – The column to use to identify unique rows in both datasets.
      • +
      • left_on (str, default=None) – The column to use to identify unique in the left dataset.
      • +
      • right_on (str, default=None) – The column to use to identify unique in the right dataset.
      • +
      • row_id_name (str, default=None) – The named column will be filled with the ids indicated for each +dataset, as per left_id/right_id/row_ids. If meta for the named +column doesn’t already exist a new column definition will be +added and assigned a reductive-appropriate type.
      • +
      • left_id (str/int/float, default=None) – Where the row_id_name column is not already populated for the +dataset_left, this value will be populated.
      • +
      • right_id (str/int/float, default=None) – Where the row_id_name column is not already populated for the +dataset_right, this value will be populated.
      • +
      • row_ids (list of str/int/float, default=None) – When datasets has been used, this list provides the row ids +that will be populated in the row_id_name column for each of +those datasets, respectively.
      • +
      • overwrite_text (bool, default=False) – If True, text_keys in the left meta that also exist in right +meta will be overwritten instead of ignored.
      • +
      • from_set (str, default=None) – Use a set defined in the right meta to control which columns are +merged from the right dataset.
      • +
      • uniquify_key (str, default None) – A int-like column name found in all the passed DataSet objects +that will be protected from having duplicates. The original version +of the column will be kept under its name prefixed with ‘original’.
      • +
      • reset_index (bool, default=True) – If True pandas.DataFrame.reindex() will be applied to the merged +dataframe.
      • +
      • inplace (bool, default True) – If True, the DataSet will be modified inplace with new/updated +rows. Will return a new DataSet instance if False.
      • +
      • verbose (bool, default=True) – Echo progress feedback to the output pane.
      • +
      +
      Returns:

      None or new_dataset – If the merge is not applied inplace, a DataSet instance +is returned.

      +
      Return type:

      quantipy.DataSet

      +
      +
      + +
      +
      +weight(weight_scheme, weight_name='weight', unique_key='identity', subset=None, report=True, path_report=None, inplace=True, verbose=True)
      +

      Weight the DataSet according to a well-defined weight scheme.

      + +++ + + + + + + + +
      Parameters:
        +
      • weight_scheme (quantipy.Rim instance) – A rim weights setup with defined targets. Can include multiple +weight groups and/or filters.
      • +
      • weight_name (str, default 'weight') – A name for the float variable that is added to pick up the weight +factors.
      • +
      • unique_key (str, default 'identity'.) – A variable inside the DataSet instance that will be used to +the map individual case weights to their matching rows.
      • +
      • subset (Quantipy complex logic expression) – A logic to filter the DataSet, weighting only the remaining subset.
      • +
      • report (bool, default True) – If True, will report a summary of the weight algorithm run +and factor outcomes.
      • +
      • path_report (str, default None) – A file path to save an .xlsx version of the weight report to.
      • +
      • inplace (bool, default True) – If True, the weight factors are merged back into the DataSet +instance. Will otherwise return the pandas.DataFrame that +contains the weight factors, the unique_key and all variables +that have been used to compute the weights (filters, target +variables, etc.).
      • +
      +
      Returns:

      Will either create a new column called 'weight' in the +DataSet instance or return a DataFrame that contains +the weight factors.

      +
      Return type:

      None or pandas.DataFrame

      +
      +
      + +
      +
      +write_dimensions(path_mdd=None, path_ddf=None, text_key=None, run=True, clean_up=True)
      +

      Build Dimensions/SPSS Base Professional .ddf/.mdd data pairs.

      +
      +

      Note

      +

      SPSS Data Collection Base Professional must be installed on +the machine. The method is creating .mrs and .dms scripts which are +executed through the software’s API.

      +
      + +++ + + + + + + + +
      Parameters:
        +
      • path_mdd (str, default None) – The full path (optionally with extension '.mdd', otherwise +assumed as such) for the saved the DataSet._meta component. +If not provided, the instance’s name and `path attributes +will be used to determine the file location.
      • +
      • path_ddf (str, default None) – The full path (optionally with extension '.ddf', otherwise +assumed as such) for the saved DataSet._data component. +If not provided, the instance’s name and `path attributes +will be used to determine the file location.
      • +
      • text_key (str, default None) – The desired text_key for all text label information. Uses +the DataSet.text_key information if not provided.
      • +
      • run (bool, default True) – If True, the method will try to run the metadata creating .mrs +script and execute a DMSRun for the case data transformation in +the .dms file.
      • +
      • clean_up (bool, default True) – By default, all helper files from the conversion (.dms, .mrs, +paired .csv files, etc.) will be deleted after the process has +finished.
      • +
      +
      Returns:

      +
      Return type:

      A .ddf/.mdd pair is saved at the provided path location.

      +
      +
      + +
      +
      +write_quantipy(path_meta=None, path_data=None)
      +

      Write the data and meta components to .csv/.json files.

      +

      The resulting files are well-defined native Quantipy source files.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_meta (str, default None) – The full path (optionally with extension '.json', otherwise +assumed as such) for the saved the DataSet._meta component. +If not provided, the instance’s name and `path attributes +will be used to determine the file location.
      • +
      • path_data (str, default None) – The full path (optionally with extension '.csv', otherwise +assumed as such) for the saved DataSet._data component. +If not provided, the instance’s name and `path attributes +will be used to determine the file location.
      • +
      +
      Returns:

      +
      Return type:

      A .csv/.json pair is saved at the provided path location.

      +
      +
      + +
      +
      +write_spss(path_sav=None, index=True, text_key=None, mrset_tag_style='__', drop_delimited=True, from_set=None, verbose=True)
      +

      Convert the Quantipy DataSet into a SPSS .sav data file.

      + +++ + + + + + + + +
      Parameters:
        +
      • path_sav (str, default None) – The full path (optionally with extension '.json', otherwise +assumed as such) for the saved the DataSet._meta component. +If not provided, the instance’s name and `path attributes +will be used to determine the file location.
      • +
      • index (bool, default False) – Should the index be inserted into the dataframe before the +conversion happens?
      • +
      • text_key (str, default None) – The text_key that should be used when taking labels from the +source meta. If the given text_key is not found for any +particular text object, the DataSet.text_key will be used +instead.
      • +
      • mrset_tag_style (str, default '__') – The delimiting character/string to use when naming dichotomous +set variables. The mrset_tag_style will appear between the +name of the variable and the dichotomous variable’s value name, +as taken from the delimited set value that dichotomous +variable represents.
      • +
      • drop_delimited (bool, default True) – Should Quantipy’s delimited set variables be dropped from +the export after being converted to dichotomous sets/mrsets?
      • +
      • from_set (str) – The set name from which the export should be drawn.
      • +
      +
      Returns:

      +
      Return type:

      A SPSS .sav file is saved at the provided path location.

      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/api_ref/QuantipyViews.html b/docs/API/_build/html/sites/api_ref/QuantipyViews.html index 466b74750..2f9e62e21 100644 --- a/docs/API/_build/html/sites/api_ref/QuantipyViews.html +++ b/docs/API/_build/html/sites/api_ref/QuantipyViews.html @@ -92,9 +92,11 @@

      Quick search

      • Release notes
          -
        • Upcoming (March)
        • -
        • Latest (27/02/2018)
        • +
        • Upcoming (September)
        • +
        • Latest (04/06/2018)
        • Archived release notes
            +
          • sd (04/04/2018)
          • +
          • sd (27/02/2018)
          • sd (12/01/2018)
          • sd (18/12/2017)
          • sd (28/11/2017)
          • @@ -202,7 +204,7 @@

            Quick search

          • Complex logic
            • union
            • intersection
            • -
            • “List” logic
            • +
            • “List” logic
            • has_any
            • not_any
            • has_all
            • @@ -371,34 +373,34 @@

              QuantipyViews Parameters:
                -
              • link (Quantipy Link object.) –
              • -
              • name (str) – The shortname applied to the view.
              • -
              • kwargs (dict) –
              • -
              • arguments (specific) (Keyword) –
              • -
              • text (str, optional, default None) – Sets an optional label in the meta component of the view that is +
              • link (Quantipy Link object.) –
              • +
              • name (str) – The shortname applied to the view.
              • +
              • kwargs (dict) –
              • +
              • arguments (specific) (Keyword) –
              • +
              • text (str, optional, default None) – Sets an optional label in the meta component of the view that is used when the view is passed into a Quantipy build (e.g. Excel, Powerpoint).
              • -
              • metric ({'props', 'means'}, default 'props') – Determines whether a proportion or means test algorithm is +
              • metric ({'props', 'means'}, default 'props') – Determines whether a proportion or means test algorithm is performed.
              • -
              • test_total (bool, deafult False) – If True, the each View’s y-axis column will be tested against the +
              • test_total (bool, deafult False) – If True, the each View’s y-axis column will be tested against the uncoditional total of its x-axis.
              • -
              • mimic ({'Dim', 'askia'}, default 'Dim') – It is possible to mimic the test logics used in other statistical +
              • mimic ({'Dim', 'askia'}, default 'Dim') – It is possible to mimic the test logics used in other statistical software packages by passing them as instructions. The method will then choose the appropriate test parameters.
              • -
              • level ({'high', 'mid', 'low'} or float) – Sets the level of significance to which the test is carried out. +
              • level ({'high', 'mid', 'low'} or float) – Sets the level of significance to which the test is carried out. Given as str the levels correspond to 'high' = 0.01, 'mid' = 0.05 and 'low' = 0.1. If a float is passed the specified level will be used.
              • -
              • flags (list of two int, default None) – Base thresholds for Dimensions-like tests, e.g. [30, 100]. First +
              • flags (list of two int, default None) – Base thresholds for Dimensions-like tests, e.g. [30, 100]. First int is minimum base for reported results, second int controls small base indication.
              Returns:

                -
              • None – Adds requested View to the Stack, storing it under the full +
              • None – Adds requested View to the Stack, storing it under the full view name notation key.
              • -
              • .. note:: – Mimicking the askia software (mimic = 'askia') +
              • .. note:: – Mimicking the askia software (mimic = 'askia') restricts the values to be one of 'high', 'low', 'mid'. Any other value passed will make the algorithm fall back to 'low'. Mimicking Dimensions (mimic = @@ -423,9 +425,9 @@

                QuantipyViews Parameters:
                  -
                • link (Quantipy Link object.) –
                • -
                • name (str) – The shortname applied to the view.
                • -
                • kwargs (dict) –
                • +
                • link (Quantipy Link object.) –
                • +
                • name (str) – The shortname applied to the view.
                • +
                • kwargs (dict) –
                @@ -451,16 +453,16 @@

                QuantipyViews Parameters: @@ -501,14 +503,14 @@

                QuantipyViews Parameters:
                  -
                • link (Quantipy Link object.) –
                • -
                • name (str) – The shortname applied to the view.
                • -
                • kwargs (dict) –
                • -
                • arguments (specific) (Keyword) –
                • -
                • text (str, optional, default None) – Sets an optional label in the meta component of the view that is +
                • link (Quantipy Link object.) –
                • +
                • name (str) – The shortname applied to the view.
                • +
                • kwargs (dict) –
                • +
                • arguments (specific) (Keyword) –
                • +
                • text (str, optional, default None) – Sets an optional label in the meta component of the view that is used when the view is passed into a Quantipy build (e.g. Excel, Powerpoint).
                • -
                • logic (list of int, list of dicts or core.tools.view.logic operation) –

                  If a list is passed this instructs a simple net of the codes given +

                • logic (list of int, list of dicts or core.tools.view.logic operation) –

                  If a list is passed this instructs a simple net of the codes given as int. Multiple nets can be generated via a list of dicts that map names to lists of ints. For complex logical statements, expression are parsed to identify the qualifying rows in the data. @@ -524,15 +526,15 @@

                  QuantipyViewsReturns:

                    -
                  • None – Adds requested View to the Stack, storing it under the full +
                  • None – Adds requested View to the Stack, storing it under the full view name notation key.
                  • -
                  • .. note:: Net codes take into account if a variable is – multi-coded. The net will therefore consider qualifying +
                  • .. note:: Net codes take into account if a variable is – multi-coded. The net will therefore consider qualifying cases and not the raw sum of the frequencies per category, i.e. no multiple counting of cases.
                  @@ -598,7 +600,7 @@

                  QuantipyViews - + diff --git a/docs/API/_build/html/sites/api_ref/Rim_scheme.html b/docs/API/_build/html/sites/api_ref/Rim_scheme.html index 5d2ca08fc..bc3ad02a9 100644 --- a/docs/API/_build/html/sites/api_ref/Rim_scheme.html +++ b/docs/API/_build/html/sites/api_ref/Rim_scheme.html @@ -92,9 +92,11 @@

                  Quick search

                  • Release notes
                      -
                    • Upcoming (March)
                    • -
                    • Latest (27/02/2018)
                    • +
                    • Upcoming (September)
                    • +
                    • Latest (04/06/2018)
                    • Archived release notes
                        +
                      • sd (04/04/2018)
                      • +
                      • sd (27/02/2018)
                      • sd (12/01/2018)
                      • sd (18/12/2017)
                      • sd (28/11/2017)
                      • @@ -202,7 +204,7 @@

                        Quick search

                      • Complex logic
                        • union
                        • intersection
                        • -
                        • “List” logic
                        • +
                        • “List” logic
                        • has_any
                        • not_any
                        • has_all
                        • @@ -366,11 +368,11 @@

                          Rim Parameters:
                            -
                          • name (str) – Name of the weight group.
                          • -
                          • filter_def (str, optional) – An optional filter defintion given as a boolean expression in +
                          • name (str) – Name of the weight group.
                          • +
                          • filter_def (str, optional) – An optional filter defintion given as a boolean expression in string format. Must be a valid input for the pandas DataFrame.query() method.
                          • -
                          • targets (dict) – Dictionary mapping of DataFrame columns to target proportion list.
                          • +
                          • targets (dict) – Dictionary mapping of DataFrame columns to target proportion list.
                          @@ -389,13 +391,13 @@

                          Rimgroup_targets(group_targets)

                          Set inter-group target proportions.

                          This will scale the weight factors per group to match the desired group -proportions and thus effectively change each group’s weighted +proportions and thus effectively change each group’s weighted total number of cases.

                          - + @@ -420,8 +422,8 @@

                          Rim

                          @@ -443,7 +445,7 @@

                          Rim

                          - @@ -507,7 +509,7 @@

                          Rim - + diff --git a/docs/API/_build/html/sites/api_ref/View.html b/docs/API/_build/html/sites/api_ref/View.html index 87854f7cb..660ab38d1 100644 --- a/docs/API/_build/html/sites/api_ref/View.html +++ b/docs/API/_build/html/sites/api_ref/View.html @@ -92,9 +92,10 @@

                          Quick search

                          Parameters:group_targets (dict) – A dictionary mapping of group names to the desired proportions.
                          Parameters:group_targets (dict) – A dictionary mapping of group names to the desired proportions.
                          Returns:
                          Parameters:
                            -
                          • targets (dict or list of dict) – Dictionary mapping of DataFrame columns to target proportion list.
                          • -
                          • group_name (str, optional) – A name for the simple weight (group) created.
                          • +
                          • targets (dict or list of dict) – Dictionary mapping of DataFrame columns to target proportion list.
                          • +
                          • group_name (str, optional) – A name for the simple weight (group) created.
                          Returns:df – A summary of missing entries and (rounded) mean/mode/median of +
                          Returns:df – A summary of missing entries and (rounded) mean/mode/median of value codes per target variable.
                          Return type:pandas.DataFrame
                          - + @@ -377,12 +378,12 @@

                          View
                          get_std_params()
                          -

                          Provides the View’s standard kwargs with fallbacks to default values.

                          +

                          Provides the View’s standard kwargs with fallbacks to default values.

                          Returns:edit_params – A tuple of kwargs controlling the following supported Link data -edits: logic, calc, ...
                          Returns:edit_params – A tuple of kwargs controlling the following supported Link data +edits: logic, calc, …
                          Return type:tuple
                          - @@ -460,12 +461,12 @@

                          View
                          meta()
                          -

                          Get a summary on a View’s meta information.

                          +

                          Get a summary on a View’s meta information.

                          Returns:std_parameters – A tuple of the common kwargs controlling the general View method +
                          Returns:std_parameters – A tuple of the common kwargs controlling the general View method behaviour: axis, relation, rel_to, weights, text
                          Return type:tuple
                          - + @@ -488,15 +489,15 @@

                          View
                          notation(method, condition)
                          -

                          Generate the View’s Stack key notation string.

                          +

                          Generate the View’s Stack key notation string.

                          Returns:viewmeta – A dictionary that contains global aggregation information.
                          Returns:viewmeta – A dictionary that contains global aggregation information.
                          Return type:dict
                          - - + @@ -513,14 +514,14 @@

                          View
                          spec_condition(link, conditionals=None, expand=None)
                          -

                          Updates the View notation’s condition component based on agg. details.

                          +

                          Updates the View notation’s condition component based on agg. details.

                          Parameters:shortname, relation (aggname,) – Strings for the aggregation name, the method’s shortname and the +
                          Parameters:shortname, relation (aggname,) – Strings for the aggregation name, the method’s shortname and the relation component of the View notation.
                          Returns:notation – The View notation.
                          Returns:notation – The View notation.
                          Return type:str
                          - + - + @@ -589,7 +590,7 @@

                          View - + diff --git a/docs/API/_build/html/sites/api_ref/ViewMapper.html b/docs/API/_build/html/sites/api_ref/ViewMapper.html index 6b6e1c34d..cbebbcad6 100644 --- a/docs/API/_build/html/sites/api_ref/ViewMapper.html +++ b/docs/API/_build/html/sites/api_ref/ViewMapper.html @@ -91,9 +91,16 @@

                          Quick search

                          @@ -389,8 +396,8 @@

                          ViewMapper

                          @@ -413,8 +420,8 @@

                          ViewMapper

                          @@ -481,7 +488,7 @@

                          ViewMapper - + diff --git a/docs/API/_build/html/sites/api_ref/quantify_engine.html b/docs/API/_build/html/sites/api_ref/quantify_engine.html index 9f5aa55cc..f8d5d7226 100644 --- a/docs/API/_build/html/sites/api_ref/quantify_engine.html +++ b/docs/API/_build/html/sites/api_ref/quantify_engine.html @@ -92,9 +92,11 @@

                          Quick search

                          - @@ -493,14 +495,14 @@

                          quantify.engine
                          rescale(scaling, drop=False)
                          -

                          Modify the object’s xdef property reflecting new value defintions.

                          +

                          Modify the object’s xdef property reflecting new value defintions.

                          Parameters:link (Link) –
                          Parameters:link (Link) –
                          Returns:relation_string – The relation part of the View name notation.
                          Returns:relation_string – The relation part of the View name notation.
                          Return type:str
                          Parameters:
                            -
                          • name (str) – The short name of the View.
                          • -
                          • method (view method) – The view method that will be used to derivce the result
                          • -
                          • kwargs (dict) – The keyword arguments needed by the view method.
                          • -
                          • template (dict) – A ViewMapper template that contains information on view method and +
                          • name (str) – The short name of the View.
                          • +
                          • method (view method) – The view method that will be used to derivce the result
                          • +
                          • kwargs (dict) – The keyword arguments needed by the view method.
                          • +
                          • template (dict) – A ViewMapper template that contains information on view method and kwargs values to iterate over.
                          Parameters:
                            -
                          • method ({'frequency', 'descriptives', 'coltests'}) – The baseline view method to be used.
                          • -
                          • iterators (dict) – A dictionary mapping of view method kwargs to lists of values.
                          • +
                          • method ({'frequency', 'descriptives', 'coltests'}) – The baseline view method to be used.
                          • +
                          • iterators (dict) – A dictionary mapping of view method kwargs to lists of values.
                          Parameters:
                            -
                          • views (list of str) – The selection of View names to keep.
                          • -
                          • strict_selection (bool, default True) – TODO
                          • +
                          • views (list of str) – The selection of View names to keep.
                          • +
                          • strict_selection (bool, default True) – TODO
                          Parameters:
                            -
                          • axis ({None, 'x', 'y'}, deafult None) – When axis is None, the frequency of all cells from the uni- or +
                          • axis ({None, 'x', 'y'}, deafult None) – When axis is None, the frequency of all cells from the uni- or multivariate distribution is presented. If the axis is specified -to be either ‘x’ or ‘y’ the margin per axis becomes the resulting +to be either ‘x’ or ‘y’ the margin per axis becomes the resulting aggregation.
                          • -
                          • raw_sum (bool, default False) – If True will perform a simple summation over the cells given the +
                          • raw_sum (bool, default False) – If True will perform a simple summation over the cells given the axis parameter. This ignores net counting of qualifying answers in favour of summing over all answers given when considering margins.
                          • -
                          • cum_sum (bool, default False) – If True a cumulative sum of the elements along the given axis is +
                          • cum_sum (bool, default False) – If True a cumulative sum of the elements along the given axis is returned.
                          • -
                          • effective (bool, default False) – If True, compute effective counts instead of traditional (weighted) +
                          • effective (bool, default False) – If True, compute effective counts instead of traditional (weighted) counts.
                          • -
                          • margin (bool, deafult True) – Controls whether the margins of the aggregation result are shown. +
                          • margin (bool, deafult True) – Controls whether the margins of the aggregation result are shown. This also applies to margin aggregations themselves, since they contain a margin in (form of the total number of cases) as well.
                          • -
                          • as_df (bool, default True) – Controls whether the aggregation is transformed into a Quantipy- +
                          • as_df (bool, default True) – Controls whether the aggregation is transformed into a Quantipy- multiindexed (following the Question/Values convention) pandas.DataFrame or will be left in its numpy.array format.
                          @@ -411,7 +413,7 @@

                          quantify.engine
                          exclude(codes, axis='x')
                          -

                          Wrapper for _missingfy(...keep_codes=False, ..., keep_base=False, ...) +

                          Wrapper for _missingfy(…keep_codes=False, …, keep_base=False, …) Excludes specified codes from aggregation.

                          @@ -430,18 +432,18 @@

                          quantify.engine

                          Parameters:
                            -
                          • groups (list, dict of lists or logic expression) –

                            The group/net code defintion(s) in form of...

                            +
                          • groups (list, dict of lists or logic expression) –

                            The group/net code defintion(s) in form of…

                            • a simple list: [1, 2, 3]
                            • a dict of list: {'grp A': [1, 2, 3], 'grp B': [4, 5, 6]}
                            • a logical expression: not_any([1, 2])
                          • -
                          • axis ({'x', 'y'}, default 'x') – The axis to group codes on.
                          • -
                          • expand ({None, 'before', 'after'}, default None) – If 'before', the codes that are grouped will be kept and placed +
                          • axis ({'x', 'y'}, default 'x') – The axis to group codes on.
                          • +
                          • expand ({None, 'before', 'after'}, default None) – If 'before', the codes that are grouped will be kept and placed before the grouped aggregation; vice versa for 'after'. Ignored on logical expressions found in groups.
                          • -
                          • complete (bool, default False) – If True, codes that define the Link on the given axis but are +
                          • complete (bool, default False) – If True, codes that define the Link on the given axis but are not present in the groups defintion(s) will be placed in their natural position within the aggregation, respecting the value of expand.
                          • @@ -461,7 +463,7 @@

                            quantify.engine
                            limit(codes, axis='x')
                            -

                            Wrapper for _missingfy(...keep_codes=True, ..., keep_base=True, ...) +

                            Wrapper for _missingfy(…keep_codes=True, …, keep_base=True, …) Restrict the data matrix entires to contain the specified codes only.

                            @@ -473,7 +475,7 @@

                            quantify.engine

                          Parameters:on ({'y', 'x'}, default 'y') – Defines the base to normalize the result on. 'y' will +
                          Parameters:on ({'y', 'x'}, default 'y') – Defines the base to normalize the result on. 'y' will produce column percentages, 'x' will produce row percentages.
                          @@ -523,14 +525,14 @@

                          quantify.engine

                          - + @@ -755,7 +768,7 @@

                          Stack
                          refresh(data_key, new_data_key='', new_weight=None, new_data=None, new_meta=None)
                          -

                          Re-run all or a portion of Stack’s aggregations for a given data key.

                          +

                          Re-run all or a portion of Stack’s aggregations for a given data key.

                          refresh() can be used to re-weight the data using a new case data weight variable or to re-run all aggregations based on a changed source data version (e.g. after cleaning the file/ dropping cases) or a @@ -771,13 +784,13 @@

                          Stack

                          @@ -800,7 +813,7 @@

                          Stack

                          - + @@ -820,8 +833,8 @@

                          Stack

                          @@ -839,15 +852,15 @@

                          Stack

                          @@ -871,13 +884,13 @@

                          Stack

                          - @@ -898,6 +911,15 @@

                          Stack

                          Parameters:
                            -
                          • scaling (dict) – Mapping of old_code: new_code, given as of type int or float.
                          • -
                          • drop (bool, default False) – If True, codes not included in the scaling dict will be excluded.
                          • +
                          • scaling (dict) – Mapping of old_code: new_code, given as of type int or float.
                          • +
                          • drop (bool, default False) – If True, codes not included in the scaling dict will be excluded.
                          Parameters:
                            -
                          • stat ({'summary', 'mean', 'median', 'var', 'stddev', 'sem', varcoeff',) – ‘min’, ‘lower_q’, ‘upper_q’, ‘max’}, default ‘summary’ +
                          • stat ({'summary', 'mean', 'median', 'var', 'stddev', 'sem', varcoeff',) – ‘min’, ‘lower_q’, ‘upper_q’, ‘max’}, default ‘summary’ The measure to calculate. Defaults to a summary output of the most important sample statistics.
                          • -
                          • axis ({'x', 'y'}, default 'x') – The axis which is reduced in the aggregation, e.g. column vs. row +
                          • axis ({'x', 'y'}, default 'x') – The axis which is reduced in the aggregation, e.g. column vs. row means.
                          • -
                          • margin (bool, default True) – Controls whether statistic(s) of the marginal distribution are +
                          • margin (bool, default True) – Controls whether statistic(s) of the marginal distribution are shown.
                          • -
                          • as_df (bool, default True) – Controls whether the aggregation is transformed into a Quantipy- +
                          • as_df (bool, default True) – Controls whether the aggregation is transformed into a Quantipy- multiindexed (following the Question/Values convention) pandas.DataFrame or will be left in its numpy.array format.
                          @@ -550,20 +552,20 @@

                          quantify.engine
                          swap(var, axis='x', update_axis_def=True, inplace=True)
                          -

                          Change the Quantity’s x- or y-axis keeping filter and weight setup.

                          +

                          Change the Quantity’s x- or y-axis keeping filter and weight setup.

                          All edits and aggregation results will be removed during the swap.

                          @@ -603,7 +605,7 @@

                          quantify.engine run()

                          Performs the testing algorithm and creates an output pd.DataFrame.

                          -

                          The output is indexed according to Quantipy’s Questions->Values +

                          The output is indexed according to Quantipy’s Questions->Values convention. Significant results between columns are presented as lists of integer y-axis codes where the column with the higher value is holding the codes of the columns with the lower values. NaN is @@ -635,7 +637,7 @@

                          quantify.engine set_params(test_total=False, level='mid', mimic='Dim', testtype='pooled', use_ebase=True, ovlp_correc=True, cwi_filter=False, flag_bases=None)

                          Sets the test algorithm parameters and defines the type of test.

                          -

                          This method sets the test’s global parameters and derives the +

                          This method sets the test’s global parameters and derives the necessary measures for the computation of the test statistic. The default values correspond to the SPSS Dimensions Column Tests algorithms that control for bias introduced by weighting and @@ -649,21 +651,21 @@

                          quantify.engine

                          @@ -403,14 +410,14 @@

                          Stack

                          @@ -427,15 +434,15 @@

                          Stack
                          -add_nets(on_vars, net_map, expand=None, calc=None, text_prefix='Net:', checking_cluster=None, _batches='all', recode=None, verbose=True)
                          +add_nets(on_vars, net_map, expand=None, calc=None, text_prefix='Net:', checking_cluster=None, _batches='all', recode='auto', verbose=True)

                          Add a net-like view to a specified collection of x keys of the stack.

                          Parameters:
                            -
                          • var (str) – New variable’s name used in axis swap.
                          • -
                          • axis ({‘x’, ‘y’}, default 'x') – The axis to swap.
                          • -
                          • update_axis_def (bool, default False) – If self is of type 'array', the name and item definitions +
                          • var (str) – New variable’s name used in axis swap.
                          • +
                          • axis ({‘x’, ‘y’}, default 'x') – The axis to swap.
                          • +
                          • update_axis_def (bool, default False) – If self is of type 'array', the name and item definitions (that are e.g. used in the to_df() method) can be updated to -reflect the swapped axis variable or kept to show the original’s +reflect the swapped axis variable or kept to show the original’s ones.
                          • -
                          • inplace (bool, default True) – Whether to modify the Quantity inplace or return a new instance.
                          • +
                          • inplace (bool, default True) – Whether to modify the Quantity inplace or return a new instance.
                          Parameters:
                          Parameters:
                            -
                          • data_key (str) – The reference name for a data source connected to the Stack.
                          • -
                          • data (pandas.DataFrame) – The input (case) data source.
                          • -
                          • meta (dict or OrderedDict) – A quantipy compatible metadata source that describes the case data.
                          • +
                          • data_key (str) – The reference name for a data source connected to the Stack.
                          • +
                          • data (pandas.DataFrame) – The input (case) data source.
                          • +
                          • meta (dict or OrderedDict) – A quantipy compatible metadata source that describes the case data.
                          Parameters:
                            -
                          • data_keys (str, optional) – The data_key to be added to. If none is given, the method will try +
                          • data_keys (str, optional) – The data_key to be added to. If none is given, the method will try to add to all data_keys found in the Stack.
                          • -
                          • filters (list of str describing filter defintions, default ['no_filter']) – The string must be a valid input for the +
                          • filters (list of str describing filter defintions, default ['no_filter']) – The string must be a valid input for the pandas.DataFrame.query() method.
                          • -
                          • y (x,) – The x and y variables to constrcut Links from.
                          • -
                          • views (list of view method names.) – Can be any of Quantipy’s preset Views or the names of created +
                          • y (x,) – The x and y variables to constrcut Links from.
                          • +
                          • views (list of view method names.) – Can be any of Quantipy’s preset Views or the names of created view method specifications.
                          • -
                          • weights (list, optional) – The names of weight variables to consider in the data aggregation +
                          • weights (list, optional) – The names of weight variables to consider in the data aggregation process. Weight variables must be of type float.
                          @@ -491,30 +498,30 @@

                          Stack
                          add_stats(on_vars, stats=['mean'], other_source=None, rescale=None, drop=True, exclude=None, factor_labels=True, custom_text=None, checking_cluster=None, _batches='all', recode=False, verbose=True)

                          Add a descriptives view to a specified collection of xks of the stack.

                          -

                          Valid descriptives views: {‘mean’, ‘stddev’, ‘min’, ‘max’, ‘median’, ‘sem’}

                          +

                          Valid descriptives views: {‘mean’, ‘stddev’, ‘min’, ‘max’, ‘median’, ‘sem’}

                          Parameters:
                          - @@ -559,18 +566,18 @@

                          Stack

                          @@ -593,10 +600,10 @@

                          Stack

                          @@ -640,16 +647,16 @@

                          Stack

                          - @@ -687,17 +694,17 @@

                          Stack

                          - @@ -717,10 +724,10 @@

                          Stack

                          @@ -734,6 +741,12 @@

                          Stack

                          Parameters:
                            -
                          • on_vars (list) – The list of x variables to add the view to.
                          • -
                          • stats (list of str, default ['mean']) – The metrics to compute and add as a view.
                          • -
                          • other_source (str) – If provided the Link’s x-axis variable will be swapped with the +
                          • on_vars (list) – The list of x variables to add the view to.
                          • +
                          • stats (list of str, default ['mean']) – The metrics to compute and add as a view.
                          • +
                          • other_source (str) – If provided the Link’s x-axis variable will be swapped with the (numerical) variable provided. This can be used to attach statistics of a different variable to a Link definition.
                          • -
                          • rescale (dict) – A dict that maps old to new codes, e.g. {1: 5, 2: 4, 3: 3, 4: 2, 5: 1}
                          • -
                          • drop (bool, default True) – If rescale is provided all codes that are not mapped will be +
                          • rescale (dict) – A dict that maps old to new codes, e.g. {1: 5, 2: 4, 3: 3, 4: 2, 5: 1}
                          • +
                          • drop (bool, default True) – If rescale is provided all codes that are not mapped will be ignored in the computation.
                          • -
                          • exclude (list) – Codes/values to ignore in the computation.
                          • -
                          • factor_labels (bool / str, default True) – Writes the (rescaled) factor values next to the category text label. -If True, square-brackets are used. If ‘()’, normal brackets are used.
                          • -
                          • custom_text (str, default None) – A custom string affix to put at the end of the requested statistics’ +
                          • exclude (list) – Codes/values to ignore in the computation.
                          • +
                          • factor_labels (bool / str, default True) – Writes the (rescaled) factor values next to the category text label. +If True, square-brackets are used. If ‘()’, normal brackets are used.
                          • +
                          • custom_text (str, default None) – A custom string affix to put at the end of the requested statistics’ names.
                          • -
                          • checking_cluster (quantipy.Cluster, default None) – When provided, an automated checking aggregation will be added to the +
                          • checking_cluster (quantipy.Cluster, default None) – When provided, an automated checking aggregation will be added to the Cluster instance.
                          • -
                          • _batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch +
                          • _batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch instances views are added.
                          • -
                          • recode (bool, default False) – Create a new variable that contains only the values +
                          • recode (bool, default False) – Create a new variable that contains only the values which are needed for the stat computation. The values and the included data will be rescaled.
                          @@ -539,7 +546,7 @@

                          Stack

                          Parameters:_batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch +
                          Parameters:_batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch instances views are added.
                          Returns:
                          Parameters:
                            -
                          • views (str or list of str or qp.ViewMapper) – views that are added.
                          • -
                          • unweighted_base (bool, default True) – If True, unweighted ‘cbase’ is added to all non-arrays. +
                          • views (str or list of str or qp.ViewMapper) – views that are added.
                          • +
                          • unweighted_base (bool, default True) – If True, unweighted ‘cbase’ is added to all non-arrays. This parameter will be deprecated in future, please use bases instead.
                          • -
                          • categorize (str or list of str) – Determines how numerical data is handled: If provided, the +
                          • categorize (str or list of str) – Determines how numerical data is handled: If provided, the variables will get counts and percentage aggregations ('counts', 'c%') alongside the 'cbase' view. If False, only 'cbase' views are generated for non-categorical types.
                          • -
                          • batches (str/ list of str, default 'all') – Name(s) of qp.Batch instance(s) that are used to aggregate the +
                          • batches (str/ list of str, default 'all') – Name(s) of qp.Batch instance(s) that are used to aggregate the qp.Stack.
                          • -
                          • xs (list of str) – Names of variable, for which views are added.
                          • -
                          • bases (dict) – Defines which bases should be aggregated, weighted or unweighted.
                          • +
                          • xs (list of str) – Names of variable, for which views are added.
                          • +
                          • bases (dict) – Defines which bases should be aggregated, weighted or unweighted.
                          Parameters:
                            -
                          • batch_name (str) – Name of the Batch whose meta_edits are taken.
                          • -
                          • data_key (str) – Accessing this metadata: self[data_key].meta +
                          • batch_name (str) – Name of the Batch whose meta_edits are taken.
                          • +
                          • data_key (str) – Accessing this metadata: self[data_key].meta Batch definitions are takes from here and this metadata is modified.
                          • -
                          • filter_key (str, default None) – Currently not implemented! +
                          • filter_key (str, default None) – Currently not implemented! Accessing this metadata: self[data_key][filter_key].meta Batch definitions are takes from here and this metadata is modified.
                          @@ -615,8 +622,8 @@

                          Stack

                          Parameters:
                            -
                          • on_vars (list) – The list of x variables to add the view to.
                          • -
                          • _batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch +
                          • on_vars (list) – The list of x variables to add the view to.
                          • +
                          • _batches (str or list of str) – Only for qp.Links that are defined in this qp.Batch instances views are added.
                          Parameters:
                            -
                          • columns (index,) – optional +
                          • columns (index,) – optional Controls the output representation by structuring a pivot-style table according to the index and column values.
                          • -
                          • query (str) – A query string that is valid for the pandas.DataFrame.query() method.
                          • -
                          • split_view_names (bool, default False) – If True, will create an output of unique view name notations split +
                          • query (str) – A query string that is valid for the pandas.DataFrame.query() method.
                          • +
                          • split_view_names (bool, default False) – If True, will create an output of unique view name notations split up into their components.
                          Returns:

                          description – DataFrame summing the Stack’s structure in terms of Links and Views.

                          +
                          Returns:

                          description – DataFrame summing the Stack’s structure in terms of Links and Views.

                          Return type:

                          pandas.DataFrame

                          @@ -668,8 +675,8 @@

                          Stack

                          Parameters:
                            -
                          • data_key (str) – Using: self[data_key]
                          • -
                          • filter_key (str, default None) – Currently not implemented! +
                          • data_key (str) – Using: self[data_key]
                          • +
                          • filter_key (str, default None) – Currently not implemented! Using: self[data_key][filter_key]
                          Parameters:
                            -
                          • data_key (str) – The data_key for the data and meta in the sav file.
                          • -
                          • filename (str) – The name to the sav file.
                          • -
                          • name (str) – A name for the sav (stored in the meta).
                          • -
                          • path (str) – The path to the sav file.
                          • -
                          • ioLocale (str) – The locale used in during the sav processing.
                          • -
                          • ioUtf8 (bool) – Boolean that indicates the mode in which text communicated to or +
                          • data_key (str) – The data_key for the data and meta in the sav file.
                          • +
                          • filename (str) – The name to the sav file.
                          • +
                          • name (str) – A name for the sav (stored in the meta).
                          • +
                          • path (str) – The path to the sav file.
                          • +
                          • ioLocale (str) – The locale used in during the sav processing.
                          • +
                          • ioUtf8 (bool) – Boolean that indicates the mode in which text communicated to or from the I/O module will be.
                          Returns:

                          stack – A stack instance that has a data_key with data and metadata +

                          Returns:

                          stack – A stack instance that has a data_key with data and metadata to run aggregations.

                          Parameters:
                            -
                          • path_stack (str) – The full path to the .stack file that should be created, including +
                          • path_stack (str) – The full path to the .stack file that should be created, including the extension.
                          • -
                          • compression ({'gzip'}, default 'gzip') – The compression type that has been used saving the file.
                          • -
                          • load_cache (bool, default False) – Loads MatrixCache into the Stack a .cache file is found.
                          • +
                          • compression ({'gzip'}, default 'gzip') – The compression type that has been used saving the file.
                          • +
                          • load_cache (bool, default False) – Loads MatrixCache into the Stack a .cache file is found.
                          +
                          +
                          +static recode_from_net_def(dataset, on_vars, net_map, expand, recode='auto', text_prefix='Net:', verbose=True)
                          +

                          Create variables from net definitions.

                          +
                          +
                          reduce(data_keys=None, filters=None, x=None, y=None, variables=None, views=None)
                          @@ -742,7 +755,7 @@

                          Stack

                          Parameters:filters, x, y, views (data_keys,) –
                          Parameters:filters, x, y, views (data_keys,) –
                          Returns:
                          Parameters:
                            -
                          • data_key (str) – The Links’ data key to be modified.
                          • -
                          • new_data_key (str, default '') – Controls if the existing data key’s files and aggregations will be +
                          • data_key (str) – The Links’ data key to be modified.
                          • +
                          • new_data_key (str, default '') – Controls if the existing data key’s files and aggregations will be overwritten or stored via a new data key.
                          • -
                          • new_weight (str) – The name of a new weight variable used to re-aggregate the Links.
                          • -
                          • new_data (pandas.DataFrame) – The case data source. If None is given, the +
                          • new_weight (str) – The name of a new weight variable used to re-aggregate the Links.
                          • +
                          • new_data (pandas.DataFrame) – The case data source. If None is given, the original case data found for the data key will be used.
                          • -
                          • new_meta (quantipy meta document) – A meta data source associated with the case data. If None is given, +
                          • new_meta (quantipy meta document) – A meta data source associated with the case data. If None is given, the original meta definition found for the data key will be used.
                          Parameters:data_keys (str or list of str) – The data keys to remove.
                          Parameters:data_keys (str or list of str) – The data keys to remove.
                          Returns:
                          Parameters:
                            -
                          • data_key (str) – Accessing this metadata: self[data_key].meta
                          • -
                          • filter_key (str, default None) – Currently not implemented! +
                          • data_key (str) – Accessing this metadata: self[data_key].meta
                          • +
                          • filter_key (str, default None) – Currently not implemented! Accessing this metadata: self[data_key][filter_key].meta
                          Parameters:
                            -
                          • path_stack (str) – The full path to the .stack file that should be created, including +
                          • path_stack (str) – The full path to the .stack file that should be created, including the extension.
                          • -
                          • compression ({'gzip'}, default 'gzip') – The intended compression type.
                          • -
                          • store_cache (bool, default True) – Stores the MatrixCache in a file in the same location.
                          • -
                          • decode_str (bool, default=True) – If True the unicoder function will be used to decode all str +
                          • compression ({'gzip'}, default 'gzip') – The intended compression type.
                          • +
                          • store_cache (bool, default True) – Stores the MatrixCache in a file in the same location.
                          • +
                          • decode_str (bool, default=True) – If True the unicoder function will be used to decode all str objects found anywhere in the meta document/s.
                          • -
                          • dataset (bool, default=False) – If True a json/csv will be saved parallel to the saved stack +
                          • dataset (bool, default=False) – If True a json/csv will be saved parallel to the saved stack for each data key in the stack.
                          • -
                          • describe (bool, default=False) – If True the result of stack.describe().to_excel() will be +
                          • describe (bool, default=False) – If True the result of stack.describe().to_excel() will be saved parallel to the saved stack.
                          Parameters:
                            -
                          • data_key (str) – The reference name of a case data source hold by the Stack instance.
                          • -
                          • only_type ({'int', 'float', 'single', 'delimited set', 'string',) – ‘date’, time’, ‘array’}, optional +
                          • data_key (str) – The reference name of a case data source hold by the Stack instance.
                          • +
                          • only_type ({'int', 'float', 'single', 'delimited set', 'string',) – ‘date’, time’, ‘array’}, optional Will restrict the output to the given data type.
                          Returns:

                          types – A summary of variable names mapped to their data types, in form of +

                          Returns:

                          types – A summary of variable names mapped to their data types, in form of {type_name: [variable names]} or a list of variable names confirming only_type.

                          ++++ + + + + + + + + + + + + + + + + + + + + + + + + + +
                          elementcontains
                          'type'case data type
                          'info'info on the source data
                          'lib'shared use references
                          'columns'info on DataFrame columns (Quantipy types, labels, etc.)
                          'sets'ordered groups of variables pointing to other parts of the meta
                          'masks'complex variable type definitions (arrays, dichotomous, etc.)
                          + +
                          +

                          columns and masks objects

                          +

                          There are two variable collections inside a Quantipy metadata document: +'columns' is storing the meta for each accompanying pandas.DataFrame +column object, while 'masks' are building upon the regular 'columns' +metadata but additionally employ special meta instructions to define +complex data types. An example is the the 'array' type that (in MR speak) maps +multiple “question” variables to one “answer” object.

                          +

                          “Simple”” data definitons that are supported by Quantipy can either be numeric +'float' and 'int' types, categorical 'single' and 'delimited set' +variables or of type 'string', 'date' and 'time'.

                          +
                          +
                          +

                          Languages: text and text_key mappings

                          +

                          Throughout Quantipy metadata all label information, e.g. variable question +texts and category descriptions, are stored in text objects that are mapping +different language (or context) versions of a label to a specific text_key. +That way the metadata can support multi-language and multi-purpose (for example +detailed/extensive vs. short question texts) label information in a digestable +format that is easy to query:

                          +
                          >>> meta['columns']['q1']['text']
                          +{'de-DE': 'Das ist ein langes deutsches Label',
                          + u'en-GB': u'What is your main fitness activity?',
                          + 'x edits': {'de-DE': 'German build label', 'en-GB': 'English build label'}}
                          +
                          +
                          +

                          Valid text_key settings are:

                          + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          text_keyLanguage / context
                          'en-GB'English
                          'de-DE'German
                          'fr-FR'French
                          'da-DK'Danish
                          'sv-SV'Swedish
                          'nb-NO'Norwegian
                          'fi-FI'Finnish
                          'x edits'Build label edit for x-axis
                          'y edits'Build label edit for y-axis
                          +
                          +
                          +

                          Categorical values object

                          +

                          single and delimited set variables restrict the possible case data +entries to a list of values that consist of numeric answer codes and their +text labels, defining distinct categories:

                          +
                          >>> meta['columns']['q1']['values']
                          +[{'value': 1,
                          +  'text': {'en-GB': 'Dog'}
                          + },
                          + {'value': 2,
                          +  'text': {'en-GB': 'Cat'}
                          + },
                          + {'value': 3,
                          +  'text': {'en-GB': 'Bird'}
                          + },
                          + {'value': -9,
                          +  'text': {'en-GB': 'Not an animal'}
                          + }]
                          +
                          +
                          +
                          +
                          +

                          The array type

                          +

                          Turning to the masks collection of the metadata, array variables +group together a collection of variables that share a common response options +scheme, i.e. different statements (usually referencing a broader topic) that +are answered using the same scale. In the Quantipy metadata document, an +array variable has a subtype that describes the type of the +constructing source variables listed in the items object. In contrast to simple variable types, any +categorical values metadata is stored inside the shared information collection +lib, for access from both the columns and masks representation of +array elements:

                          +
                          >>> meta['masks']['q5']
                          +{u'items': [{u'source': u'columns@q5_1', u'text': {u'en-GB': u'Surfing'}},
                          +  {u'source': u'columns@q5_2', u'text': {u'en-GB': u'Snowboarding'}},
                          +  {u'source': u'columns@q5_3', u'text': {u'en-GB': u'Kite boarding'}},
                          +  {u'source': u'columns@q5_4', u'text': {u'en-GB': u'Parachuting'}},
                          +  {u'source': u'columns@q5_5', u'text': {u'en-GB': u'Cave diving'}},
                          +  {u'source': u'columns@q5_6', u'text': {u'en-GB': u'Windsurfing'}}],
                          + u'name': u'q5',
                          + u'subtype': u'single',
                          + u'text': {u'en-GB': u'How likely are you to do each of the following in the next year?'},
                          + u'type': u'array',
                          + u'values': 'lib@values@q5'}
                          +
                          +
                          +
                          >>> meta['lib']['values']['q5']
                          +[{u'text': {u'en-GB': u'I would refuse if asked'}, u'value': 1},
                          + {u'text': {u'en-GB': u'Very unlikely'}, u'value': 2},
                          + {u'text': {u'en-GB': u"Probably wouldn't"}, u'value': 3},
                          + {u'text': {u'en-GB': u'Probably would if asked'}, u'value': 4},
                          + {u'text': {u'en-GB': u'Very likely'}, u'value': 5},
                          + {u'text': {u'en-GB': u"I'm already planning to"}, u'value': 97},
                          + {u'text': {u'en-GB': u"Don't know"}, u'value': 98}]
                          +
                          +
                          +

                          Exploring the columns meta of an array item shows the same values reference pointer and informs about its parent meta structure, i.e. the +array’s masks defintion:

                          +
                          >>> meta['columns']['q5_1']
                          +{u'name': u'q5_1',
                          + u'parent': {u'masks@q5': {u'type': u'array'}},
                          + u'text': {u'en-GB': u'How likely are you to do each of the following in the next year? - Surfing'},
                          + u'type': u'single',
                          + u'values': u'lib@values@q5'}
                          +
                          +
                          +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html new file mode 100644 index 000000000..2ff1e17ae --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html @@ -0,0 +1,656 @@ + + + + + + + + + + + I/O — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          I/O

                          +
                          +

                          Starting from native components

                          +
                          +

                          Using a standalone pd.DataFrame

                          +

                          Quantipy can create a meta document from a inferring its variable types from +the dtypes of a pd.DataFrame. In that process, ìnt, float and +string data types are created inside the meta component of the DataSet. +In this basic form, text label information is missing. For a example, given +a pd.DataFrame as per:

                          +
                          >>> casedata = [[1000, 10, 1.2, 'text1'],
                          +...             [1001, 4, 3.4, 'jjda'],
                          +...             [1002, 8, np.NaN, 'what?'],
                          +...             [1003, 8, 7.81, '---' ],
                          +...             [1004, 5, 3.0, 'hello world!']]
                          +>>> df = pd.DataFrame(casedata, columns=['identity', 'q1', 'q2', 'q3'])
                          +>>> df
                          +   identity  q1    q2            q3
                          +0      1000  10  1.20         text1
                          +1      1001   4  3.40          jjda
                          +2      1002   8   NaN         what?
                          +3      1003   8  7.81           ---
                          +4      1004   5  3.00  hello world!
                          +
                          +
                          +

                          … the conversion is adding matching metadata to the DataSet instance:

                          +
                          >>> dataset = qp.DataSet(name='example', dimensions_comp=False)
                          +>>> dataset.from_components(df)
                          +Inferring meta data from pd.DataFrame.columns (4)...
                          +identity: dtype: int64 - converted: int
                          +q1: dtype: int64 - converted: int
                          +q2: dtype: float64 - converted: float
                          +q3: dtype: object - converted: string
                          +
                          +
                          +
                          >>> dataset.meta()['columns']['q2']
                          +{'text': {'en-GB': ''}, 'type': 'float', 'name': 'q2', 'parent': {}, 'properties': {'created': True}}
                          +
                          +
                          +
                          +
                          +

                          .csv / .json pairs

                          +

                          We can easily read in Quantipy native data with the read_quantipy() +method and providing the paths to both the .csv and .json file (file +extensions are handled automatically), e.g.:

                          +
                          >>> folder = './Data/'
                          +>>> file_name = 'Example Data (A)'
                          +>>> path_csv = path_json = folder + file_name
                          +
                          +
                          +
                          >>> dataset = qp.DataSet(name='example', dimensions_comp=False)
                          +>>> dataset.read_quantipy(path_json, path_csv)
                          +DataSet: ./Data/example
                          +rows: 8255 - columns: 76
                          +Dimensions compatibility mode: False
                          +
                          +
                          +

                          We can that access the case and metadata components:

                          +
                          >>> dataset.data()['q4'].head()
                          +0    1
                          +1    2
                          +2    2
                          +3    1
                          +4    1
                          +Name: q4, dtype: int64
                          +
                          +
                          +
                          >>> meta = dataset.meta()['columns']['q4']
                          +>>> json.dumps(meta)
                          +{
                          +    "values": [
                          +        {
                          +            "text": {
                          +                "en-GB": "Yes"
                          +            },
                          +            "value": 1
                          +        },
                          +        {
                          +            "text": {
                          +                "en-GB": "No"
                          +            },
                          +            "value": 2
                          +        }
                          +    ],
                          +    "text": {
                          +        "en-GB": "Do you ever participate in sports activities with people in your household?"
                          +    },
                          +    "type": "single",
                          +    "name": "q4",
                          +    "parent": {}
                          +}
                          +
                          +
                          +
                          +
                          +
                          +

                          Third party conversions

                          +
                          +

                          Supported conversions

                          +

                          In adddition to providing plain .csv/.json data (pairs), source files +can be read into Quantipy using a number of I/O functions to deal with +standard file formats encountered in the market research industry:

                          + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          SoftwareFormatReadWrite
                          SPSS +Statistics.savYesYes
                          SPSS +Dimensions.dff/.mddYesYes
                          Deciphertab-delimited +.json/ .txtYesNo
                          Ascribetab-delimited +.xml/ .txtYesNo
                          +

                          The following functions are designed to convert the different file formats’ +structures into inputs understood by Quantipy.

                          +
                          +
                          +

                          SPSS Statistics

                          +

                          Reading:

                          +
                          >>> from quantipy.core.tools.dp.io import read_spss
                          +>>> meta, data = read_spss(path_sav)
                          +
                          +
                          +
                          +

                          Note

                          +

                          On a Windows machine you MUST use ioLocale=None when reading +from SPSS. This means if you are using a Windows machine your base +example for reading from SPSS is +meta, data = read_spss(path_sav, ioLocale=None).

                          +
                          +

                          When reading from SPSS you have the opportunity to specify a custom +dichotomous values map, that will be used to convert all dichotomous +sets into Quantipy delimited sets, using the dichot argument.

                          +

                          The entire read operation will use the same map on all dichotomous +sets so they must be applied uniformly throughout the SAV file. The +default map that will be used if none is provided will be +{'yes': 1, 'no': 0}.

                          +
                          >>> meta, data = read_spss(path_sav, dichot={'yes': 1, 'no': 2})
                          +
                          +
                          +

                          SPSS dates will be converted to pandas dates by default but +if this results in conversion issues or failures you can read +the dates in as Quantipy strings to deal with them later, using the +dates_as_strings argument.

                          +
                          >>> meta, data = read_spss(path_sav, dates_as_strings=True)
                          +
                          +
                          +

                          Writing:

                          +
                          >>> from quantipy.core.tools.dp.io import write_spss
                          +>>> write_spss(path_sav, meta, data)
                          +
                          +
                          +

                          By default SPSS files will be generated from the 'data file' +set found in meta['sets'], but a custom set can be named instead +using the from_set argument.

                          +
                          >>> write_spss(path_sav_analysis, meta, data, from_set='sav-export')
                          +
                          +
                          +

                          The custom set must be well-formed:

                          +
                          >>> "sets" : {
                          +...     "sav-export": {
                          +...         "items": [
                          +...             "columns@Q1",
                          +...             "columns@Q2",
                          +...             "columns@Q3",
                          +...             ...
                          +...         ]
                          +...     }
                          +... }
                          +
                          +
                          +
                          +
                          +

                          Dimensions

                          +

                          Reading:

                          +
                          >>> from quantipy.core.tools.dp.io import read_dimensions
                          +>>> meta, data = read_dimensions(path_mdd, path_ddf)
                          +
                          +
                          +
                          +
                          +

                          Decipher

                          +

                          Reading:

                          +
                          >>> from quantipy.core.tools.dp.io import read_decipher
                          +>>> meta, data = read_decipher(path_json, path_txt)
                          +
                          +
                          +
                          +
                          +

                          Ascribe

                          +

                          Reading:

                          +
                          >>> from quantipy.core.tools.dp.io import read_ascribe
                          +>>> meta, data = read_ascribe(path_xml, path_txt)
                          +
                          +
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html new file mode 100644 index 000000000..5d5383bdd --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html @@ -0,0 +1,532 @@ + + + + + + + + + + + DataSet management — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          DataSet management

                          +
                          +

                          Setting the variable order

                          +

                          The global variable order of a DataSet is dictated by the content of the +meta['sets']['data file']['items'] list and reflected in the structure of +the case data component’s pd.DataFrame.columns. There are two ways to set +a new order using the order(new_order=None, reposition=None) method:

                          +

                          Define a full order

                          +

                          Using this apporach requires that all DataSet variable names are passed +via the new_order parameter. Providing only a subset of the variables will +raise a ValueError:

                          +
                          >>> dataset.order(['q1', 'q8'])
                          +ValueError: 'new_order' must contain all DataSet variables.
                          +
                          +
                          +

                          Text…

                          +

                          Change positions relatively

                          +

                          Often only a few changes to the natural order of the DataSet are necessary, +e.g. derived variables should be moved alongside their originating ones or specific +sets of variables (demographics, etc.) should be grouped together. We can achieve +this using the reposition parameter as follows:

                          +

                          Text…

                          +
                          +
                          +

                          Cloning, filtering and subsetting

                          +

                          Sometimes you want to cut the data into sections defined by either case/respondent conditions (e.g. a survey wave) or a collection of variables (e.g. +a specific part of the questionnaire). To not permanently change an existing +DataSet by accident, draw a copy of it first:

                          +
                          >>> copy_ds = dataset.clone()
                          +
                          +
                          +

                          Then you can use filter() to restrict cases (rows) or subset() to keep +only a selected range of variables (columns). Both methods can be used inplace +but will return a new object by default.

                          +
                          >>> keep = {'Wave': [1]}
                          +>>> copy_ds.filter(alias='first wave', condition=keep, inplace=True)
                          +>>> copy_ds._data.shape
                          +(1621, 76)
                          +
                          +
                          +

                          After the filter has been applied, the DataSet is only showing cases that contain the value 1 in the 'Wave' variable. The filter alias (a short name +to describe the arbitrarily complex filter condition) is attached to the +instance:

                          +
                          >>> copy_ds.filtered
                          +only first wave
                          +
                          +
                          +

                          We are now further reducing the DataSet by dropping all variables except the three array variables 'q5', 'q6', and 'q7' using subset().

                          +
                          >>> reduced_ds = copy_ds.subset(variables=['q5', 'q6', 'q7'])
                          +
                          +
                          +

                          We can see that only the requested variables (masks defintitions and the +constructing array items) remain in reduced_ds:

                          +
                          >>> reduced_ds.by_type()
                          +size: 1621 single delimited set array int float string date time N/A
                          +0            q5_1                  q5
                          +1            q5_2                  q7
                          +2            q5_3                  q6
                          +3            q5_4
                          +4            q5_5
                          +5            q5_6
                          +6            q6_1
                          +7            q6_2
                          +8            q6_3
                          +9            q7_1
                          +10           q7_2
                          +11           q7_3
                          +12           q7_4
                          +13           q7_5
                          +14           q7_6
                          +
                          +
                          +
                          +
                          +

                          Merging

                          +

                          Intro text… As opposed to reducing an existing file…

                          +
                          +

                          Vertical (cases/rows) merging

                          +

                          Text

                          +
                          +
                          +

                          Horizontal (variables/columns) merging

                          +

                          Text

                          +
                          +
                          +
                          +

                          Savepoints and state rollback

                          +

                          When working with big DataSets and needing to perform a lot of data +preparation (deriving large amounts of new variables, lots of meta editing, +complex cleaning, …) it can be beneficial to quickly store a snapshot of a +clean and consistent state of the DataSet. This is most useful when working +in interactive sessions like IPython or Jupyter notebooks and might +prevent you from reloading files from disk or waiting for previous processes +to finish.

                          +

                          Savepoints are stored via save() and can be restored via revert().

                          +
                          +

                          Note

                          +

                          Savepoints only exists in memory and are not written to disk. Only one +savepoint can exist, so repeated save() calls will overwrite any previous +versions of the DataSet. To permanently save your data, please use one +of the write methods, e.g. write_quantipy().

                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html new file mode 100644 index 000000000..38f0bd22d --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html @@ -0,0 +1,775 @@ + + + + + + + + + + + Inspecting variables — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Inspecting variables

                          +
                          +

                          Querying and slicing case data

                          +

                          A qp.DataSet is mimicking pandas-like item access, i.e. passing a variable +name into the []-accessor will return a pandas.DataFrame view of the +case data component. That means that we can chain any pandas.DataFrame method to +the query:

                          +
                          >>> ds['q9'].head()
                          +     q9
                          +0   99;
                          +1  1;4;
                          +2   98;
                          +3  1;4;
                          +4   99;
                          +
                          +
                          +

                          There is the same support for selecting multiple variables at once:

                          +
                          >>> ds[['q9', 'gender']].head()
                          +     q9  gender
                          +0   99;       1
                          +1  1;4;       2
                          +2   98;       1
                          +3  1;4;       1
                          +4   99;       1
                          +
                          +
                          +

                          To integrate array (masks) variables into this behaviour, passing an +array name will automatically call its item list:

                          +
                          >>> ds['q6'].head()
                          +   q6_1  q6_2  q6_3
                          +0     1     1     1
                          +1     1   NaN     1
                          +2     1   NaN     2
                          +3     2   NaN     2
                          +4     2    10    10
                          +
                          +
                          +

                          This can be combined with the list-based selection as well:

                          +
                          >>> ds[['q6', 'q9', 'gender']].head()
                          +   q6_1  q6_2  q6_3    q9  gender
                          +0     1     1     1   99;       1
                          +1     1   NaN     1  1;4;       2
                          +2     1   NaN     2   98;       1
                          +3     2   NaN     2  1;4;       1
                          +4     2    10    10   99;       1
                          +
                          +
                          +

                          DataSet case data supports row-slicing based on complex logical conditions +to inspect subsets of the data. We can use the take() with a Quantipy +logic operation naturally for this:

                          +
                          >>> condition = intersection(
                          +...    [{'gender': [1]},
                          +...     {'religion': [3]},
                          +...     {'q9': [1, 4]}])
                          +>>> take = ds.take(condition)
                          +
                          +
                          +
                          >>> ds[take, ['gender', 'religion', 'q9']].head()
                          +     gender  religion      q9
                          +52        1         3  1;2;4;
                          +357       1         3  1;3;4;
                          +671       1         3  1;3;4;
                          +783       1         3  2;3;4;
                          +802       1         3      4;
                          +
                          +
                          +
                          +

                          See also

                          +

                          Please find an overview of Quantipy logical operators and data slicing +and masking in the docs about complex logical conditions!

                          +
                          +
                          +
                          +

                          Variable and value existence

                          +

                          any, all, code_count, is_nan, var_exists, codes_in_data, is_like_numeric +variables

                          +
                          +

                          We can use variables() and var_exists() to generally test the membership +of variables inside DataSet. The former is showing the list of all variables +registered inside the 'data file' set, the latter is checking if a variable’s +name is found in either the 'columns' or 'masks' collection. For +our example data, the variables are:

                          +
                          >>> dataset.variables()
                          +
                          +
                          +

                          So a test for the array 'q5' should be positive:

                          +
                          >>> dataset.var_exists('q5')
                          +True
                          +
                          +
                          +

                          In addition to Quantipy’s complex logic operators, the DataSet class +offers some quick case data operations for code existence tests. To return a +pandas.Series of all empty rows inside a variable use is_nan() as per:

                          +
                          >>> dataset.is_nan('q8').head()
                          +0    True
                          +1    True
                          +2    True
                          +3    True
                          +4    True
                          +Name: q8, dtype: bool
                          +
                          +
                          +

                          Which we can also use to quickly check the number of missing cases…

                          +
                          >>> dataset.is_nan('q8').value_counts()
                          +True     5888
                          +False    2367
                          +Name: q8, dtype: int64
                          +
                          +
                          +

                          … as well as use the result as slicer for the DataSet case data component, +e.g. to show the non-empty rows:

                          +
                          >>> slicer = dataset.is_nan('q8')
                          +>>> dataset[~slicer, 'q8'].head()
                          +Name: q8, dtype: int64
                          +7       5;
                          +11      5;
                          +13    1;4;
                          +14    4;5;
                          +23    1;4;
                          +Name: q8, dtype: object
                          +
                          +
                          +

                          Especially useful for delimited set and array data, the code_count() +method is creating the pandas.Series of response values found. If applied on +an array, the result is expressed across all source item variables:

                          +
                          >>> dataset.code_count('q6').value_counts()
                          +3    5100
                          +2    3155
                          +dtype: int64
                          +
                          +
                          +

                          … which means that not all cases contain answers in all three of the array’s items.

                          +

                          With some basic pandas we can double-check this result:

                          +
                          >>> pd.concat([dataset['q6'], dataset.code_count('q6')], axis=1).head()
                          +   q6_1  q6_2  q6_3  0
                          +0     1   1.0     1  3
                          +1     1   NaN     1  2
                          +2     1   NaN     2  2
                          +3     2   NaN     2  2
                          +4     2  10.0    10  3
                          +
                          +
                          +

                          code_count() can optionally ignore certain codes via the count_only and +count_not parameters:

                          +
                          >>> q2_count = dataset.code_count('q2', count_only=[1, 2, 3])
                          +>>> pd.concat([dataset['q2'], q2_count], axis=1).head()
                          +         q2  0
                          +0  1;2;3;5;  3
                          +1      3;6;  1
                          +2       NaN  0
                          +3       NaN  0
                          +4       NaN  0
                          +
                          +
                          +

                          Similarly, the any() and all() methods yield slicers for cases obeying +the condition that at least one / all of the provided codes are found in the +response. Again, for array variables the conditions are extended across all +the items:

                          +
                          >>> dataset[dataset.all('q6', 5), 'q6']
                          +      q6_1  q6_2  q6_3
                          +374      5   5.0     5
                          +2363     5   5.0     5
                          +2377     5   5.0     5
                          +4217     5   5.0     5
                          +5530     5   5.0     5
                          +5779     5   5.0     5
                          +5804     5   5.0     5
                          +6328     5   5.0     5
                          +6774     5   5.0     5
                          +7269     5   5.0     5
                          +8148     5   5.0     5
                          +
                          +
                          +
                          >>> dataset[dataset.all('q8', [1, 2, 3, 4, 96]), 'q8']
                          +845     1;2;3;4;5;96;
                          +6242      1;2;3;4;96;
                          +7321      1;2;3;4;96;
                          +Name: q8, dtype: object
                          +
                          +
                          +
                          >>> dataset[dataset.any('q8', [1, 2, 3, 4, 96]), 'q8'].head()
                          +13      1;4;
                          +14      4;5;
                          +23      1;4;
                          +24    1;3;4;
                          +25      1;4;
                          +Name: q8, dtype: object
                          +
                          +
                          +
                          +
                          +

                          Variable types

                          +

                          To get a summary of the all variables grouped by type, call by_type() on +the DataSet:

                          +
                          >>> ds.by_type()
                          +size: 8255     single delimited set array            int     float string        date      time N/A
                          +0              gender            q2    q5  record_number    weight    q8a  start_time  duration
                          +1            locality            q3    q7      unique_id  weight_a    q9a    end_time
                          +2           ethnicity            q8    q6            age  weight_b
                          +3            religion            q9            birth_day
                          +4                  q1                        birth_month
                          +5                 q2b                         birth_year
                          +6                  q4
                          +7                q5_1
                          +8                q5_2
                          +9                q5_3
                          +10               q5_4
                          +11               q5_5
                          +12               q5_6
                          +13               q6_1
                          +14               q6_2
                          +15               q6_3
                          +16               q7_1
                          +17               q7_2
                          +18               q7_3
                          +19               q7_4
                          +20               q7_5
                          +21               q7_6
                          +
                          +
                          +

                          We can restrict the output to certain types by providing the desired ones in +the types parameter:

                          +
                          >>> ds.by_type(types='delimited set')
                          +size: 8255 delimited set
                          +0                     q2
                          +1                     q3
                          +2                     q8
                          +3                     q9
                          +
                          +
                          +
                          >>> ds.by_type(types=['delimited set', 'float'])
                          +size: 8255 delimited set     float
                          +0                     q2    weight
                          +1                     q3  weight_a
                          +2                     q8  weight_b
                          +3                     q9       NaN
                          +
                          +
                          +

                          In addition to that, DataSet implements the following methods +that return the corresponding variables as a list for easy iteration:

                          +
                          DataSet.singles
                          +       .delimied_sets()
                          +       .ints()
                          +       .floats()
                          +       .dates()
                          +       .strings()
                          +       .masks()
                          +       .columns()
                          +       .sets()
                          +
                          +
                          +
                          >>> ds.delimited_sets()
                          +[u'q3', u'q2', u'q9', u'q8']
                          +
                          +
                          +
                          >>> for delimited_set in ds.delimited_sets():
                          +...     print delimited_set
                          +q3
                          +q2
                          +q9
                          +q8
                          +
                          +
                          +
                          +
                          +

                          Slicing & dicing metadata objects

                          +

                          Although it is possible to access a DataSet meta component via its _meta +attribute directly, the prefered way to inspect and interact with with the metadata +is to use DataSet methods. For instance, the easiest way to view the most +important meta on a variable is to use the meta() method:

                          +
                          >>> ds.meta('q8')
                          +delimited set                                      codes                         texts missing
                          +q8: Which of the following do you regularly skip?
                          +1                                                      1                     Breakfast    None
                          +2                                                      2          Mid-morning snacking    None
                          +3                                                      3                         Lunch    None
                          +4                                                      4        Mid-afternoon snacking    None
                          +5                                                      5                        Dinner    None
                          +6                                                     96                  None of them    None
                          +7                                                     98  Don't know (it varies a lot)    None
                          +
                          +
                          +

                          This output is extended with the item metadata if an array is passed:

                          +
                          >>> ds.meta('q6')
                          +single                                             items                   item texts  codes                        texts missing
                          +q6: How often do you take part in any of the fo...
                          +1                                                   q6_1               Exercise alone      1     Once a day or more often    None
                          +2                                                   q6_2       Join an exercise class      2               Every few days    None
                          +3                                                   q6_3  Play any kind of team sport      3                  Once a week    None
                          +4                                                                                          4             Once a fortnight    None
                          +5                                                                                          5                 Once a month    None
                          +6                                                                                          6        Once every few months    None
                          +7                                                                                          7        Once every six months    None
                          +8                                                                                          8                  Once a year    None
                          +9                                                                                          9  Less often than once a year    None
                          +10                                                                                        10                        Never    None
                          +
                          +
                          +

                          If the variable is not categorical, meta() returns simply:

                          +
                          >>> ds.meta('weight_a')
                          +                             float
                          +weight_a: Weight (variant A)   N/A
                          +
                          +
                          +

                          DataSet also provides a lot of methods to access and return the several +meta objects of a variable to make various data processing tasks easier:

                          +

                          Variable labels: quantipy.core.dataset.DataSet.text()

                          +
                          >>> ds.text('q8', text_key=None)
                          +Which of the following do you regularly skip?
                          +
                          +
                          +

                          values object: quantipy.core.dataset.DataSet.values()

                          +
                          >>> ds.values('gender', text_key=None)
                          +[(1, u'Male'), (2, u'Female')]
                          +
                          +
                          +

                          Category codes: quantipy.core.dataset.DataSet.codes()

                          +
                          >>> ds.codes('gender')
                          +[1, 2]
                          +
                          +
                          +

                          Category labels: quantipy.core.dataset.DataSet.value_texts()

                          +
                          >>> ds.value_texts('gender', text_key=None)
                          +[u'Male', u'Female']
                          +
                          +
                          +

                          items object: quantipy.core.dataset.DataSet.items()

                          +
                          >>> ds.items('q6', text_key=None)
                          +[(u'q6_1', u'How often do you exercise alone?'),
                          + (u'q6_2', u'How often do you take part in an exercise class?'),
                          + (u'q6_3', u'How often do you play any kind of team sport?')]
                          +
                          +
                          +

                          Item 'columns' sources: quantipy.core.dataset.DataSet.sources()

                          +
                          >>> ds.sources('q6')
                          +[u'q6_1', u'q6_2', u'q6_3']
                          +
                          +
                          +

                          Item labels: quantipy.core.dataset.DataSet.item_texts()

                          +
                          >>> ds.item_texts('q6', text_key=None)
                          +[u'How often do you exercise alone?',
                          + u'How often do you take part in an exercise class?',
                          + u'How often do you play any kind of team sport?']
                          +
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/04_editing.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/04_editing.html new file mode 100644 index 000000000..f9af45717 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/04_editing.html @@ -0,0 +1,631 @@ + + + + + + + + + + + Editing metadata — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Editing metadata

                          +
                          +

                          Creating meta from scratch

                          +

                          It is very easy to add new variable metadata to a DataSet via add_meta() +which let’s you create all supported variable types. Each new variable needs at +least a name, qtype and label. With this information a string, +int, float or date variable can be defined, e.g.:

                          +
                          >>> ds.add_meta(name='new_int', qtype='int', label='My new int variable')
                          +>>> ds.meta('new_int')
                          +                              int
                          +new_int: My new int variable  N/A
                          +
                          +
                          +

                          Using the categories parameter we can create categorical variables of type +single or delimited set. We can provide the categories in two +different ways:

                          +
                          >>> name, qtype, label = 'new_single', 'single', 'My new single variable'
                          +
                          +
                          +

                          Providing a list of category labels (codes will be enumerated starting +from 1):

                          +
                          >>> cats = ['Category A', 'Category B', 'Category C']
                          +
                          +
                          +
                          >>> ds.add_meta(name, qtype, label, categories=cats)
                          +>>> ds.meta('new_single')
                          +single                              codes       texts missing
                          +new_single: My new single variable
                          +1                                       1  Category A    None
                          +2                                       2  Category B    None
                          +3                                       3  Category C    None
                          +
                          +
                          +

                          Providing a list of tuples pairing codes and labels:

                          +
                          >>> cats = [(1, 'Category A'), (2, 'Category B'), (99, 'Category C')]
                          +
                          +
                          +
                          >>> ds.add_meta(name, qtype, label, categories=cats)
                          +>>> ds.meta('new_single')
                          +single                              codes       texts missing
                          +new_single: My new single variable
                          +1                                       1  Category A    None
                          +2                                       2  Category B    None
                          +3                                      99  Category C    None
                          +
                          +
                          +
                          +

                          Note

                          +

                          add_meta() is preventing you from adding ill-formed or +inconsistent variable information, e.g. it is not possible to add categories +to an int

                          +
                          >>> ds.add_meta('new_int', 'int', 'My new int variable', cats)
                          +ValueError: Numerical data of type int does not accept 'categories'.
                          +
                          +
                          +

                          …and you must provide categories when trying to add categorical data:

                          +
                          >>> ds.add_meta(name, 'single', label, categories=None)
                          +ValueError: Must provide 'categories' when requesting data of type single.
                          +
                          +
                          +
                          +

                          Similiar to the usage of the categories argument, items is controlling +the creation of an array, i.e. specifying items is automatically +preparing the 'masks' and 'columns' metadata. The qtype argument +in this case always refers to the type of the corresponding 'columns'.

                          +
                          >>> name, qtype, label = 'new_array', 'single', 'My new array variable'
                          +>>> cats = ['Category A', 'Category B', 'Category C']
                          +
                          +
                          +

                          Again, there are two alternatives to construct the items object:

                          +

                          Providing a list of item labels (item identifiers will be enumerated +starting from 1):

                          +
                          >>> items = ['Item A', 'Item B', 'Item C', 'Item D']
                          +
                          +
                          +
                          >>> ds.add_meta(name, qtype, label, cats, items=items)
                          +>>> ds.meta('new_array')
                          +single                                  items item texts codes       texts missing
                          +new_array: My new array variable
                          +1                                 new_array_1     Item A     1  Category A    None
                          +2                                 new_array_2     Item B     2  Category B    None
                          +3                                 new_array_3     Item C     3  Category C    None
                          +4                                 new_array_4     Item D
                          +
                          +
                          +

                          Providing a list of tuples pairing item identifiers and labels:

                          +
                          >>> items = [(1, 'Item A'), (2, 'Item B'), (97, 'Item C'), (98, 'Item D')]
                          +
                          +
                          +
                          >>> ds.add_meta(name, qtype, label, cats, items)
                          +>>> ds.meta('new_array')
                          +single                                   items item texts codes       texts missing
                          +new_array: My new array variable
                          +1                                  new_array_1     Item A     1  Category A    None
                          +2                                  new_array_2     Item B     2  Category B    None
                          +3                                 new_array_97     Item C     3  Category C    None
                          +4                                 new_array_98     Item D
                          +
                          +
                          +
                          +

                          Note

                          +

                          For every created variable, add_meta() is also adding the relevant columns +into the pd.DataFrame case data component of the DataSet to keep +it consistent:

                          +
                          >>> ds['new_array'].head()
                          +           new_array_1  new_array_2  new_array_97  new_array_98
                          +0          NaN          NaN           NaN           NaN
                          +1          NaN          NaN           NaN           NaN
                          +2          NaN          NaN           NaN           NaN
                          +3          NaN          NaN           NaN           NaN
                          +4          NaN          NaN           NaN           NaN
                          +
                          +
                          +
                          +
                          +
                          +

                          Renaming

                          +

                          It is possible to attach new names to DataSet variables. Using the rename() +method will replace all former variable keys and other mentions inside the +metadata document and exchange the DataFrame column names. For array +variables only the 'masks' name reference is updated by default – to rename +the corresponding items a dict mapping item position number to new name can +be provided.

                          +
                          >>> ds.rename(name='q8', new_name='q8_with_a_new_name')
                          +
                          +
                          +

                          As mentioned, renaming a 'masks' variable will leave the items untouched:

                          +
                          >>>
                          +
                          +
                          +

                          But we can simply provide their new names as per:

                          +
                          >>>
                          +
                          +
                          +
                          >>>
                          +
                          +
                          +
                          +
                          +

                          Changing & adding text info

                          +

                          All text-related DataSet methods expose the text_key argument to +control to which language or context a label is added. For instance we can add +a German variable label to 'q8' with set_variable_text():

                          +
                          >>> ds.set_variable_text(name='q8', new_text='Das ist ein deutsches Label', text_key='de-DE')
                          +
                          +
                          +
                          >>> ds.text('q8', 'en-GB')
                          +Which of the following do you regularly skip?
                          +
                          +
                          +
                          >>> ds.text('q8', 'de-DE')
                          +Das ist ein deutsches Label
                          +
                          +
                          +

                          To change the text inside the values or items metadata, we can +similarly use set_value_text and set_item_text():

                          +
                          >>>
                          +
                          +
                          +

                          When working with multiple language versions of the metadata, it might be required +to copy one language’s text meta to another one’s, for instance if there are +no fitting translations or the correct translation is missing. In such cases you +can use force_texts() to copy the meta of a source text_key (specified +in the `copy_from parameter) to a target text_key (indicated via copy_to).

                          +
                          >>>
                          +
                          +
                          +
                          >>>
                          +
                          +
                          +

                          With clean_texts() you also have the option to replace specific characters, +terms or formatting tags (i.e. html) from all text metadata of the +DataSet:

                          +
                          >>>
                          +
                          +
                          +
                          +
                          +

                          Extending the values object

                          +

                          We can add new category defintitons to existing values meta with the +extend_values() method. As when adding full metadata for categorical +variables, new values can be generated by either providing only labels or +tuples of codes and labels.

                          +
                          >>>
                          +
                          +
                          +

                          While the method will never allow adding duplicated numeric values for the +categories, setting safe to False will enable you to add duplicated text +meta, i.e. values could contain both +{'text': {'en-GB': 'No answer'}, 'value': 98} and +{'text': {'en-GB': 'No answer'}, 'value': 99}. By default, however, +the method will strictly prohibit any duplicates in the resulting values.

                          +
                          >>>
                          +
                          +
                          +
                          +
                          +

                          Reordering the values object

                          +
                          +
                          +

                          Removing DataSet objects

                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/05_transforming.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/05_transforming.html new file mode 100644 index 000000000..166727f1a --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/05_transforming.html @@ -0,0 +1,843 @@ + + + + + + + + + + + Transforming variables — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +

                          Transforming variables

                          +
                          +
                          +
                          +

                          Copying

                          +

                          It’s often recommended to draw a clean copy of a variable before starting to +editing its meta or case data. With copy() you can add a copy to the +DataSet that is identical to the original in all respects but its name. By +default, the copy’s name will be suffixed with '_rec', but you can apply a +custom suffix by providing it via the suffix argument (leaving out the +'_' which is added automatically):

                          +
                          >>> ds.copy('q3')
                          +>>> ds.copy('q3', suffix='version2')
                          +
                          +
                          +
                          >>> ds.delimited_sets
                          +[u'q3', u'q2', u'q9', u'q8', u'q3_rec', u'q3_version2']
                          +
                          +
                          +

                          Querying the DataSet, we can see that all three version are looking identical:

                          +
                          >>> ds[['q3', 'q3_rec', 'q3_version2']].head()
                          +       q3  q3_rec q3_version2
                          +0  1;2;3;  1;2;3;      1;2;3;
                          +1  1;2;3;  1;2;3;      1;2;3;
                          +2  1;2;3;  1;2;3;      1;2;3;
                          +3    1;3;    1;3;        1;3;
                          +4      2;      2;          2;
                          +
                          +
                          +

                          We can, however, prevent copying the case data and simply add an “empty” copy +of the variable by passing copy_data=False:

                          +
                          >>> ds.copy('q3', suffix='no_data', copy_data=False)
                          +
                          +
                          +
                          >>> ds[['q3', 'q3_rec', 'q3_version2', 'q3_no_data']].head()
                          +       q3  q3_rec q3_version2  q3_no_data
                          +0  1;2;3;  1;2;3;      1;2;3;         NaN
                          +1  1;2;3;  1;2;3;      1;2;3;         NaN
                          +2  1;2;3;  1;2;3;      1;2;3;         NaN
                          +3    1;3;    1;3;        1;3;         NaN
                          +4      2;      2;          2;         NaN
                          +
                          +
                          +

                          If we wanted to only copy a subset of the case data, we could also use a +logical slicer and supply it in the copy() operation’s +slicer parameter:

                          +
                          >>> slicer = {'gender': [1]}
                          +>>> ds.copy('q3', suffix='only_men', copy_data=True, slicer=slicer)
                          +
                          +
                          +
                          >>> ds[['q3', 'gender', 'q3_only_men']].head()
                          +       q3  gender q3_only_men
                          +0  1;2;3;       1      1;2;3;
                          +1  1;2;3;       2         NaN
                          +2  1;2;3;       1      1;2;3;
                          +3    1;3;       1        1;3;
                          +4      2;       1          2;
                          +
                          +
                          +
                          +
                          +

                          Inplace type conversion

                          +

                          You can change the characteristics of existing DataSet variables by +converting from one type to another. Conversions happen inplace, i.e. +no copy of the variable is taken prior to the operation. Therefore, you might +want to take a DataSet.copy() before using the convert(name, to) +method.

                          +

                          Conversions need to modify both the meta and data component of the +DataSet and are limited to transformations that keep the original and new +state of a variable consistent. The following conversions are currently +supported:

                          + ++++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          name (from-type)to='single'to='delimited set'to='int'to='float'to='string'
                          'single'[X]XXXX
                          'delimited set' [X]   
                          'int'X [X]XX
                          'float'   [X]X
                          'string'X X*X*[X]
                          'date'X   X
                          +

                          * If all values of the variable are numerical, i.e. DataSet.is_like_numeric() returns True.

                          +

                          Each of these conversions will rebuild the variable meta data to match the to +type. This means, that for instance a variable that is single will lose +its values object when transforming to int, while the reverse operation +will create a values object that categorizes the unqiue numeric codes found in the +case data with their str representation as text meta. Consider the +variables q1 (single) and age (int):

                          +

                          From type single to int:

                          +
                          >>> ds.meta('q1')
                          +single                                   codes                                   texts missing
                          +q1: What is your main fitness activity?
                          +1                                            1                                Swimming    None
                          +2                                            2                         Running/jogging    None
                          +3                                            3                         Lifting weights    None
                          +4                                            4                                Aerobics    None
                          +5                                            5                                    Yoga    None
                          +6                                            6                                 Pilates    None
                          +7                                            7                       Football (soccer)    None
                          +8                                            8                              Basketball    None
                          +9                                            9                                  Hockey    None
                          +10                                          96                                   Other    None
                          +11                                          98  I regularly change my fitness activity    None
                          +12                                          99       Not applicable - I don't exercise    None
                          +
                          +
                          +
                          >>> ds.convert('q1', to='int')
                          +>>> ds.meta('q1')
                          +                                         int
                          +q1: What is your main fitness activity?  N/A
                          +
                          +
                          +

                          From type int to single:

                          +
                          >>> ds.meta('age')
                          +          int
                          +age: Age  N/A
                          +
                          +
                          +
                          >>> ds.convert('age', to='single')
                          +>>> ds.meta('age')
                          +single    codes texts missing
                          +age: Age
                          +1            19    19    None
                          +2            20    20    None
                          +3            21    21    None
                          +4            22    22    None
                          +5            23    23    None
                          +6            24    24    None
                          +7            25    25    None
                          +8            26    26    None
                          +9            27    27    None
                          +10           28    28    None
                          +11           29    29    None
                          +12           30    30    None
                          +13           31    31    None
                          +14           32    32    None
                          +15           33    33    None
                          +16           34    34    None
                          +17           35    35    None
                          +18           36    36    None
                          +19           37    37    None
                          +20           38    38    None
                          +21           39    39    None
                          +22           40    40    None
                          +23           41    41    None
                          +24           42    42    None
                          +25           43    43    None
                          +26           44    44    None
                          +27           45    45    None
                          +28           46    46    None
                          +29           47    47    None
                          +30           48    48    None
                          +31           49    49    None
                          +
                          +
                          +
                          +
                          +

                          Banding and categorization

                          +

                          In contrast to convert(), the categorize() method creates a new +variable of type single, acting as a short-hand for creating a renamed copy +and then type-transforming it. Therefore, it lets you quickly categorize +the unique values of a text, int or date variable, storing +values meta in the form of {'text': {'en-GB': str(1)}, 'value': 1}.

                          +
                          >>>
                          +
                          +
                          +

                          Flexible banding of numeric data is provided thorugh DataSet.band(): If a +variable is banded, it will standardly be added to the DataSet via the +original’s name suffixed with 'banded', e.g. 'age_banded', keeping +the originating variables text label. The new_name and label +parameters can be used to create custom variable names and labels. The banding +of the incoming data is controlled with the bands argument that expects a +list containing int, tuples or dict, where each type is used for a +different kind of group definition.

                          +

                          Banding with int and tuple:

                          +
                            +
                          • Use an int to make a band of only one value
                          • +
                          • Use a tuple to indicate (inclusive) group limits
                          • +
                          • values text meta is infered
                          • +
                          • Example: [0, (1, 10), (11, 14), 15, (16, 25)]
                          • +
                          +

                          Banding with dict:

                          +
                            +
                          • The dict key will dicate the group’s text label meta
                          • +
                          • The dict value can pick up an int / tuple (see above)
                          • +
                          • Example: [{'A': 0}, {'B': (1, 10)}, {'C': (11, 14)}, {'D': 15}, {'E': (16, 25)}]
                          • +
                          • Mixing allowed: [0, {'A': (1, 10)}, (11, 14), 15, {'B': (16, 25)}]
                          • +
                          +

                          For instance, we could band 'age' into a new variable called 'grouped_age' +with bands being:

                          +
                          >>> bands = [{'Younger than 35': (19, 34)},
                          +...          (35, 39),
                          +...          {'Exactly 40': 40},
                          +...          41,
                          +...          (42, 60)]
                          +
                          +
                          +
                          >>> ds.band(name='age', bands=bands, new_name='grouped_age', label=None)
                          +
                          +
                          +
                          >>> ds.meta('grouped_age')
                          +single            codes            texts missing
                          +grouped_age: Age
                          +1                     1  Younger than 35    None
                          +2                     2            35-39    None
                          +3                     3       Exactly 40    None
                          +4                     4               41    None
                          +5                     5            42-60    None
                          +
                          +
                          +
                          >>> ds.crosstab('age', 'grouped_age')
                          +Question        grouped_age. Age
                          +Values                       All Younger than 35 35-39 Exactly 40   41 42-60
                          +Question Values
                          +age. Age All                8255            4308  1295        281  261  2110
                          +         19                  245             245     0          0    0     0
                          +         20                  277             277     0          0    0     0
                          +         21                  270             270     0          0    0     0
                          +         22                  323             323     0          0    0     0
                          +         23                  272             272     0          0    0     0
                          +         24                  263             263     0          0    0     0
                          +         25                  246             246     0          0    0     0
                          +         26                  252             252     0          0    0     0
                          +         27                  260             260     0          0    0     0
                          +         28                  287             287     0          0    0     0
                          +         29                  270             270     0          0    0     0
                          +         30                  271             271     0          0    0     0
                          +         31                  264             264     0          0    0     0
                          +         32                  287             287     0          0    0     0
                          +         33                  246             246     0          0    0     0
                          +         34                  275             275     0          0    0     0
                          +         35                  258               0   258          0    0     0
                          +         36                  236               0   236          0    0     0
                          +         37                  252               0   252          0    0     0
                          +         38                  291               0   291          0    0     0
                          +         39                  258               0   258          0    0     0
                          +         40                  281               0     0        281    0     0
                          +         41                  261               0     0          0  261     0
                          +         42                  290               0     0          0    0   290
                          +         43                  267               0     0          0    0   267
                          +         44                  261               0     0          0    0   261
                          +         45                  257               0     0          0    0   257
                          +         46                  259               0     0          0    0   259
                          +         47                  243               0     0          0    0   243
                          +         48                  271               0     0          0    0   271
                          +         49                  262               0     0          0    0   262
                          +
                          +
                          +
                          +
                          +

                          Array transformations

                          +

                          Transposing arrays

                          +

                          DataSet offers tools to simplify common array variable operations. +You can switch the structure of items vs. values by producing the one +from the other using transpose(). The transposition of an array will always +result in items that have the delimited set type in the corresponding +'columns' metadata. That is because the transposed array is collecting +what former items have been assignd per former value:

                          +
                          >>> ds.transpose('q5')
                          +
                          +
                          +

                          Original

                          +
                          >>> ds['q5'].head()
                          +   q5_1  q5_2  q5_3  q5_4  q5_5  q5_6
                          +0     2     2     2     2     1     2
                          +1     5     5     3     3     3     5
                          +2     5    98     5     5     1     5
                          +3     5     5     1     5     3     5
                          +4    98    98    98    98    98    98
                          +
                          +
                          +
                          >>> ds.meta('q5')
                          +single                                             items     item texts  codes                    texts missing
                          +q5: How likely are you to do each of the follow...
                          +1                                                   q5_1        Surfing      1  I would refuse if asked    None
                          +2                                                   q5_2   Snowboarding      2            Very unlikely    None
                          +3                                                   q5_3  Kite boarding      3        Probably wouldn't    None
                          +4                                                   q5_4    Parachuting      4  Probably would if asked    None
                          +5                                                   q5_5    Cave diving      5              Very likely    None
                          +6                                                   q5_6    Windsurfing     97  I'm already planning to    None
                          +7                                                                           98               Don't know    None
                          +
                          +
                          +

                          Transposition

                          +
                          >>> ds['q5_trans'].head()
                          +  q5_trans_1  q5_trans_2 q5_trans_3 q5_trans_4 q5_trans_5 q5_trans_97   q5_trans_98
                          +0         5;  1;2;3;4;6;        NaN        NaN        NaN         NaN           NaN
                          +1        NaN         NaN     3;4;5;        NaN     1;2;6;         NaN           NaN
                          +2         5;         NaN        NaN        NaN   1;3;4;6;         NaN            2;
                          +3         3;         NaN         5;        NaN   1;2;4;6;         NaN           NaN
                          +4        NaN         NaN        NaN        NaN        NaN         NaN  1;2;3;4;5;6;
                          +
                          +
                          +
                          >>> ds.meta('q5_trans')
                          +delimited set                                             items               item texts codes          texts missing
                          +q5_trans: How likely are you to do each of the ...
                          +1                                                    q5_trans_1  I would refuse if asked     1        Surfing    None
                          +2                                                    q5_trans_2            Very unlikely     2   Snowboarding    None
                          +3                                                    q5_trans_3        Probably wouldn't     3  Kite boarding    None
                          +4                                                    q5_trans_4  Probably would if asked     4    Parachuting    None
                          +5                                                    q5_trans_5              Very likely     5    Cave diving    None
                          +6                                                   q5_trans_97  I'm already planning to     6    Windsurfing    None
                          +7                                                   q5_trans_98               Don't know
                          +
                          +
                          +

                          The method’s ignore_items and ignore_values arguments can pick up +items (indicated by their order number) and values to leave aside +during the transposition.

                          +

                          Ignoring items

                          +

                          The new values meta’s numerical codes will always be enumerated from 1 to +the number of valid items for the transposition, so ignoring items 2, 3 and 4 +will lead to:

                          +
                          >>> ds.transpose('q5', ignore_items=[2, 3, 4])
                          +
                          +
                          +
                          >>> ds['q5_trans'].head(1)
                          +  q5_trans_1 q5_trans_2 q5_trans_3 q5_trans_4 q5_trans_5 q5_trans_97 q5_trans_98
                          +0         2;       1;3;        NaN        NaN        NaN         NaN         NaN
                          +
                          +
                          +
                          >>> ds.values('q5_trans')
                          +[(1, 'Surfing'), (2, 'Cave diving'), (3, 'Windsurfing')]
                          +
                          +
                          +

                          Ignoring values

                          +
                          >>> ds.transpose('q5', ignore_values=[1, 97])
                          +
                          +
                          +
                          >>> ds['q5_trans'].head(1)
                          +   q5_trans_2 q5_trans_3 q5_trans_4 q5_trans_5 q5_trans_98
                          +0  1;2;3;4;6;        NaN        NaN        NaN         NaN
                          +
                          +
                          +
                          >>> ds.items('q5_trans')
                          +[('q5_trans_2', u'Very unlikely'),
                          + ('q5_trans_3', u"Probably wouldn't"),
                          + ('q5_trans_4', u'Probably would if asked'),
                          + ('q5_trans_5', u'Very likely'),
                          + ('q5_trans_98', u"Don't know")]
                          +
                          +
                          +

                          Ignoring both items and values

                          +
                          >>> ds.transpose('q5', ignore_items=[2, 3, 4], ignore_values=[1, 97])
                          +
                          +
                          +
                          >>> ds['q5_trans'].head(1)
                          +  q5_trans_2 q5_trans_3 q5_trans_4 q5_trans_5 q5_trans_98
                          +0       1;3;        NaN        NaN        NaN         NaN
                          +
                          +
                          +
                          >>> ds.meta('q5_trans')
                          +delimited set                                             items               item texts codes        texts missing
                          +q5_trans: How likely are you to do each of the ...
                          +1                                                    q5_trans_2            Very unlikely     1      Surfing    None
                          +2                                                    q5_trans_3        Probably wouldn't     2  Cave diving    None
                          +3                                                    q5_trans_4  Probably would if asked     3  Windsurfing    None
                          +4                                                    q5_trans_5              Very likely
                          +5                                                   q5_trans_98               Don't know
                          +
                          +
                          +

                          Flatten item answers

                          +
                            +
                          • flatten()
                          • +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/06_logics.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/06_logics.html new file mode 100644 index 000000000..adaac7c1e --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/06_logics.html @@ -0,0 +1,589 @@ + + + + + + + + + + + Logic and set operaters — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Logic and set operaters

                          +
                          +

                          Ranges

                          +

                          The frange() function takes a string of abbreviated ranges, possibly delimited +by a comma (or some other character) and extrapolates its full, +unabbreviated list of ints.

                          +
                          >>> from quantipy.core.tools.dp.prep import frange
                          +
                          +
                          +

                          Basic range:

                          +
                          >>> frange('1-5')
                          +[1, 2, 3, 4, 5]
                          +
                          +
                          +

                          Range in reverse:

                          +
                          >>> frange('15-11')
                          +[15, 14, 13, 12, 11]
                          +
                          +
                          +

                          Combination:

                          +
                          >>> frange('1-5,7,9,15-11')
                          +[1, 2, 3, 4, 5, 7, 9, 15, 14, 13, 12, 11]
                          +
                          +
                          +

                          May include spaces for clarity:

                          +
                          >>> frange('1-5, 7, 9, 15-11')
                          +[1, 2, 3, 4, 5, 7, 9, 15, 14, 13, 12, 11]
                          +
                          +
                          +
                          +
                          +

                          Complex logic

                          +

                          Multiple conditions can be combined using union or intersection set +statements. Logical mappers can be arbitrarily nested as long as they are +well-formed.

                          +
                          +

                          union

                          +

                          union takes a list of logical conditions that will be treated with +or logic.

                          +

                          Where any of logic_A, logic_B or logic_C are True:

                          +
                          >>> union([logic_A, logic_B, logic_C])
                          +
                          +
                          +
                          +
                          +

                          intersection

                          +

                          intersection takes a list of conditions that will be +treated with and logic.

                          +

                          Where all of logic_A, logic_B and logic_C are True:

                          +
                          >>> intersection([logic_A, logic_B, logic_C])
                          +
                          +
                          +
                          +
                          +

                          “List” logic

                          +

                          Instead of using the verbose has_any operator, we can express simple, non-nested +or logics simply as a list of codes. For example {"q1_1": [1, 2]} is an +example of list-logic, where [1, 2] will be interpreted as has_any([1, 2]), +meaning if q1_1 has any of the values 1 or 2.

                          +

                          q1_1 has any of the responses 1, 2 or 3:

                          +
                          >>> l = {"q1_1": [1, 2, 3]}
                          +
                          +
                          +
                          +
                          +

                          has_any

                          +

                          q1_1 has any of the responses 1, 2 or 3:

                          +
                          >>> l = {"q1_1": has_any([1, 2, 3])}
                          +
                          +
                          +

                          q1_1 has any of the responses 1, 2 or 3 and no others:

                          +
                          >>> l = {"q1_1": has_any([1, 2, 3], exclusive=True)}
                          +
                          +
                          +
                          +
                          +

                          not_any

                          +

                          q1_1 doesn’t have any of the responses 1, 2 or 3:

                          +
                          >>> l = {"q1_1": not_any([1, 2, 3])}
                          +
                          +
                          +

                          q1_1 doesn’t have any of the responses 1, 2 or 3 but has some others:

                          +
                          >>> l = {"q1_1": not_any([1, 2, 3], exclusive=True)}
                          +
                          +
                          +
                          +
                          +

                          has_all

                          +

                          q1_1 has all of the responses 1, 2 and 3:

                          +
                          >>> l = {"q1_1": has_all([1, 2, 3])}
                          +
                          +
                          +

                          q1_1 has all of the responses 1, 2 and 3 and no others:

                          +
                          >>> l = {"q1_1": has_all([1, 2, 3], exclusive=True)}
                          +
                          +
                          +
                          +
                          +

                          not_all

                          +

                          q1_1 doesn’t have all of the responses 1, 2 and 3:

                          +
                          >>> l = {"q1_1": not_all([1, 2, 3])}
                          +
                          +
                          +

                          q1_1 doesn’t have all of the responses 1, 2 and 3 but has some others:

                          +
                          >>> l = {"q1_1": not_all([1, 2, 3], exclusive=True)}
                          +
                          +
                          +
                          +
                          +

                          has_count

                          +

                          q1_1 has exactly 2 responses:

                          +
                          >>> l = {"q1_1": has_count(2)}
                          +
                          +
                          +

                          q1_1 has 1, 2 or 3 responses:

                          +
                          >>> l = {"q1_1": has_count([1, 3])}
                          +
                          +
                          +

                          q1_1 has 1 or more responses:

                          +
                          >>> l = {"q1_1": has_count([is_ge(1)])}
                          +
                          +
                          +

                          q1_1 has 1, 2 or 3 responses from the response group 5, 6, 7, 8 or 9:

                          +
                          >>> l = {"q1_1": has_count([1, 3, [5, 6, 7, 8, 9]])}
                          +
                          +
                          +

                          q1_1 has 1 or more responses from the response group 5, 6, 7, 8 or 9:

                          +
                          >>> l = {"q1_1": has_count([is_ge(1), [5, 6, 7, 8, 9]])}
                          +
                          +
                          +
                          +
                          +

                          not_count

                          +

                          q1_1 doesn’t have exactly 2 responses:

                          +
                          >>> l = {"q1_1": not_count(2)}
                          +
                          +
                          +

                          q1_1 doesn’t have 1, 2 or 3 responses:

                          +
                          >>> l = {"q1_1": not_count([1, 3])}
                          +
                          +
                          +

                          q1_1 doesn’t have 1 or more responses:

                          +
                          >>> l = {"q1_1": not_count([is_ge(1)])}
                          +
                          +
                          +

                          q1_1 doesn’t have 1, 2 or 3 responses from the response group 5, 6, 7, 8 or 9:

                          +
                          >>> l = {"q1_1": not_count([1, 3, [5, 6, 7, 8, 9]])}
                          +
                          +
                          +

                          q1_1 doesn’t have 1 or more responses from the response group 5, 6, 7, 8 or 9:

                          +
                          >>> l = {"q1_1": not_count([is_ge(1), [5, 6, 7, 8, 9]])}
                          +
                          +
                          +
                          +
                          +
                          +

                          Boolean slicers and code existence

                          +

                          any(), all() +code_count(), is_nan()

                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/07_custom_recoding.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/07_custom_recoding.html new file mode 100644 index 000000000..6aa703602 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/07_custom_recoding.html @@ -0,0 +1,1036 @@ + + + + + + + + + + + Custom data recoding — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Custom data recoding

                          +
                          +

                          The recode() method in detail

                          +

                          This function takes a mapper of {key: logic} entries and injects the +key into the target column where its paired logic is True. The logic +may be arbitrarily complex and may refer to any other variable or +variables in data. Where a pre-existing column has been used to +start the recode, the injected values can replace or be appended to +any data found there to begin with. Note that this function does +not edit the target column, it returns a recoded copy of the target +column. The recoded data will always comply with the column type +indicated for the target column according to the meta.

                          + +++ + + + +
                          method:recode(target, mapper, default=None, append=False, +intersect=None, initialize=None, fillna=None, inplace=True)
                          +
                          +

                          target

                          +

                          target controls which column meta should be used to control the +result of the recode operation. This is important because you cannot +recode multiple responses into a ‘single’-typed column.

                          +

                          The target column must already exist in meta.

                          +

                          The recode function is effectively a request to return a copy of +the target column, recoded as instructed. recode does not +edit the target column in place, it returns a recoded copy of it.

                          +

                          If the target column does not already exist in data then a new +series, named accordingly and initialized with np.NaN, will begin +the recode.

                          +

                          Return a recoded version of the column radio_stations_xb edited +based on the given mapper:

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper=mapper
                          +... )
                          +
                          +
                          +

                          By default, recoded data resulting from the the mapper will replace any +data already sitting in the target column (on a cell-by-cell basis).

                          +
                          +
                          +

                          mapper

                          +

                          A mapper is a dict of {value: logic} entries where value represents +the data that will be injected for cases where the logic is True.

                          +

                          Here’s a simplified example of what a mapper looks like:

                          +
                          >>> mapper = {
                          +...     1: logic_A,
                          +...     2: logic_B,
                          +...     3: logic_C,
                          +... }
                          +
                          +
                          +

                          1 will be generated where logic_A is True, 2 where logic_B is +True and 3 where logic_C is True.

                          +

                          The recode function, by referencing the type indicated by the meta, +will manage the complications involved in single vs delimited set +data.

                          +
                          >>> mapper = {
                          +...     901: {'radio_stations': frange('1-13')},
                          +...     902: {'radio_stations': frange('14-20')},
                          +...     903: {'radio_stations': frange('21-25')}
                          +... }
                          +
                          +
                          +

                          This means: inject 901 if the column radio_stations has any of the +values 1-13, 902 where radio_stations has any of the values 14-20 +and 903 where radio_stations has any of the values 21-25.

                          +
                          +
                          +

                          default

                          +

                          If you had lots of values to generate from the same reference column +(say most/all of them were based on radio_stations) then we can +omit the wildcard logic format and use recode’s default parameter.

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25')
                          +...     },
                          +...     default='radio_stations'
                          +... )
                          +
                          +
                          +

                          This means, all unkeyed logic will default to be keyed to +radio_stations. In this case the three codes 901, 902 and 903 will +be generated based on the data found in radio_stations.

                          +

                          You can combine this with reference to other columns, but you can only +provide one default column.

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25'),
                          +...         904: {'age': frange('18-34')}
                          +...     },
                          +...     default='radio_stations'
                          +... )
                          +
                          +
                          +

                          Given that logic can be arbitrarily complicated, mappers can be as +well. You’ll see an example of a mapper that recodes a segmentation +in Example 4, below.

                          +
                          +
                          +

                          append

                          +

                          If you want the recoded data to be appended to whatever may +already be in the target column (this is only applicable for ‘delimited +set’-typed columns), then you should use the append parameter.

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper=mapper,
                          +...     append=True
                          +... )
                          +
                          +
                          +

                          The precise behaviour of the append parameter can be seen in the +following examples.

                          +

                          Given the following data:

                          +
                          >>> df['radio_stations_xb']
                          +1    6;7;9;13;
                          +2          97;
                          +3          97;
                          +4    13;16;18;
                          +5         2;6;
                          +Name: radio_stations_xb, dtype: object
                          +
                          +
                          +

                          We generate a recoded value of 901 if any of the values 1-13 are +found. With the default append=False behaviour we will return the +following:

                          +
                          >>> target = 'radio_stations_xb'
                          +>>> recode(meta, data, target, mapper)
                          +1    901;
                          +2     97;
                          +3     97;
                          +4    901;
                          +5    901;
                          +Name: radio_stations_xb, dtype: object
                          +
                          +
                          +

                          However, if we instead use append=True, we will return the following:

                          +
                          >>> target = 'radio_stations_xb'
                          +>>> recode(meta, data, target, mapper, append=True)
                          +1    6;7;9;13;901;
                          +2              97;
                          +3              97;
                          +4    13;16;18;901;
                          +5         2;6;901;
                          +Name: radio_stations_xb, dtype: object
                          +
                          +
                          +
                          +
                          +

                          intersect

                          +

                          One way to help simplify complex logical conditions, especially when +they are in some way repetitive, is to use intersect, which +accepts any logical statement and forces every condition in the mapper +to become the intersection of both it and the intersect condition.

                          +

                          For example, we could limit our recode to males by giving a logical +condition to that effect to intersect:

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25'),
                          +...         904: {'age': frange('18-34')}
                          +...     },
                          +...     default='radio_stations',
                          +...     intersect={'gender': [1]}
                          +... )
                          +
                          +
                          +
                          +
                          +

                          initialize

                          +

                          You may also initialize your copy of the target column as part of your +recode operation. You can initalize with either np.NaN (to overwrite +anything that may already be there when your recode begins) or by naming +another column. When you name another column a copy of the data from that +column is used to initialize your recode.

                          +

                          Initialization occurs before your recode.

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25'),
                          +...         904: {'age': frange('18-34')}
                          +...     },
                          +...     default='radio_stations',
                          +...     initialize=np.NaN
                          +... )
                          +
                          +
                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25'),
                          +...         904: {'age': frange('18-34')}
                          +...     },
                          +...     default='radio_stations',
                          +...     initialize='radio_stations'
                          +... )
                          +
                          +
                          +
                          +
                          +

                          fillna

                          +

                          You may also provide a fillna value that will be used as per +pd.Series.fillna() after the recode has been performed.

                          +
                          >>> recoded = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: frange('1-13'),
                          +...         902: frange('14-20'),
                          +...         903: frange('21-25'),
                          +...         904: {'age': frange('18-34')}
                          +...     },
                          +...     default='radio_stations',
                          +...     initialize=np.NaN,
                          +...     fillna=99
                          +... )
                          +
                          +
                          +
                          +
                          +
                          +

                          Custom recode examples

                          +
                          +

                          Building a net code

                          +

                          Here’s an example of copying an existing question and recoding onto it a +net code.

                          +

                          Create the new metadata:

                          +
                          >>> meta['columns']['radio_stations_xb'] = copy.copy(
                          +...     meta['columns']['radio_stations']
                          +... )
                          +>>> meta['columns']['radio_stations_xb']['values'].append(
                          +...     {
                          +...         "value": 901,
                          +...         "text": {"en-GB": "NET: Listened to radio in past 30 days"}
                          +...     }
                          +... )
                          +
                          +
                          +

                          Initialize the new column. In this case we’re starting with a copy of +the radio_stations column:

                          +
                          >>> data['radio_stations_xb'] = data['radio_stations'].copy()
                          +
                          +
                          +

                          Recode the new column by appending the code 901 to it as indicated +by the mapper:

                          +
                          >>> data['radio_stations_xb'] = recode(
                          +...     meta, data,
                          +...     target='radio_stations_xb',
                          +...     mapper={
                          +...         901: {'radio_stations': frange('1-23, 92, 94, 141')}
                          +...     },
                          +...     append=True
                          +... )
                          +
                          +
                          +

                          Check the result:

                          +
                          >>> data[['radio_stations', 'radio_stations_xb']].head(20)
                          +   radio_stations radio_stations_cb
                          +0              5;            5;901;
                          +1             97;               97;
                          +2             97;               97;
                          +3             97;               97;
                          +4             97;               97;
                          +5              4;            4;901;
                          +6             11;           11;901;
                          +7              4;            4;901;
                          +8             97;               97;
                          +9             97;               97;
                          +10            97;               97;
                          +11            92;           92;901;
                          +12            97;               97;
                          +13       1;13;17;      1;13;17;901;
                          +14             6;            6;901;
                          +15      1;5;6;10;     1;5;6;10;901;
                          +16             6;            6;901;
                          +17        2;4;16;       2;4;16;901;
                          +18          6;10;         6;10;901;
                          +19             6;            6;901;
                          +
                          +
                          +
                          +
                          +

                          Create-and-fill

                          +

                          Here’s an example where the value 1 is generated based on some logic +and then all remaining cases are given the value 2 using the +pandas.Series.fillna() method.

                          +

                          Create the new metadata

                          +
                          >>> meta['columns']['age_xb'] = {
                          +...     'type': 'single',
                          +...     'text': {'en-GB': 'Age'},
                          +...     'values': [
                          +...         {'value': 1, 'text': {'en-GB': '16-25'}},
                          +...         {'value': 2, 'text': {'en-GB': 'Others'}}
                          +...     ]
                          +...     }
                          +
                          +
                          +

                          Initialize the new column:

                          +
                          >>> data['age_xb'] = np.NaN
                          +
                          +
                          +

                          Recode the new column:

                          +
                          >>> data['age_xb'] = recode(
                          +...     meta, data,
                          +...     target='age_xb',
                          +...     mapper={
                          +...         1: {'age': frange('16-40')}
                          +...     }
                          +... )
                          +
                          +
                          +

                          Fill all cases that are still empty with the value 2:

                          +
                          >>> data['age_xb'].fillna(2, inplace=True)
                          +
                          +
                          +

                          Check the result:

                          +
                          >>> data[['age', 'age_xb']].head(20)
                          +    age  age_grp_rc
                          +0    22           1
                          +1    68           2
                          +2    32           1
                          +3    44           2
                          +4    33           1
                          +5    52           2
                          +6    54           2
                          +7    44           2
                          +8    62           2
                          +9    49           2
                          +10   64           2
                          +11   73           2
                          +12   43           2
                          +13   28           1
                          +14   66           2
                          +15   39           1
                          +16   51           2
                          +17   50           2
                          +18   77           2
                          +19   42           2
                          +
                          +
                          +
                          +
                          +

                          Numerical banding

                          +

                          Here’s a typical example of recoding age into custom bands.

                          +

                          In this case we’re using list comprehension to generate the first ten +values objects and then concatenate that with a final ‘65+’ value object +which doesn’t folow the same label format.

                          +

                          Create the new metadata:

                          +
                          >>> meta['columns']['age_xb_1'] = {
                          +...     'type': 'single',
                          +...     'text': {'en-GB': 'Age'},
                          +...     'values': [
                          +...         {
                          +...             'value': i,
                          +...             'text': {'en-GB': '{}-{}'.format(r[0], r[1])}
                          +...         }
                          +...         for i, r in enumerate(
                          +...             [
                          +...                 [18, 20],
                          +...                 [21, 25], [26, 30],
                          +...                 [31, 35], [36, 40],
                          +...                 [41, 45], [46, 50],
                          +...                 [51, 55], [56, 60],
                          +...                 [61, 65]
                          +...             ],
                          +...             start=1
                          +...         )
                          +...     ] + [
                          +...         {
                          +...             'value': 11,
                          +...             'text': {'en-GB': '65+'}
                          +...         }
                          +...     ]
                          +... }
                          +
                          +
                          +

                          Initialize the new column:

                          +
                          >>> data['age_xb_1'] = np.NaN
                          +
                          +
                          +

                          Recode the new column:

                          +
                          >>> data['age_xb_1'] = recode(
                          +...     meta, data,
                          +...     target='age_xb_1',
                          +...     mapper={
                          +...         1: frange('18-20'),
                          +...         2: frange('21-25'),
                          +...         3: frange('26-30'),
                          +...         4: frange('31-35'),
                          +...         5: frange('36-40'),
                          +...         6: frange('41-45'),
                          +...         7: frange('46-50'),
                          +...         8: frange('51-55'),
                          +...         9: frange('56-60'),
                          +...         10: frange('61-65'),
                          +...         11: frange('66-99')
                          +...     },
                          +...     default='age'
                          +... )
                          +
                          +
                          +

                          Check the result:

                          +
                          >>> data[['age', 'age_xb_1']].head(20)
                          +    age  age_cb
                          +0    22       2
                          +1    68      11
                          +2    32       4
                          +3    44       6
                          +4    33       4
                          +5    52       8
                          +6    54       8
                          +7    44       6
                          +8    62      10
                          +9    49       7
                          +10   64      10
                          +11   73      11
                          +12   43       6
                          +13   28       3
                          +14   66      11
                          +15   39       5
                          +16   51       8
                          +17   50       7
                          +18   77      11
                          +19   42       6
                          +
                          +
                          +
                          +
                          +

                          Complicated segmentation

                          +

                          Here’s an example of using a complicated, nested series of logic +statements to recode an obscure segmentation.

                          +

                          The segemenation was given with the following definition:

                          +

                          1 - Self-directed:

                          +
                            +
                          • If q1_1 in [1,2] and q1_2 in [1,2] and q1_3 in [3,4,5]
                          • +
                          +

                          2 - Validators:

                          +
                            +
                          • If q1_1 in [1,2] and q1_2 in [1,2] and q1_3 in [1,2]
                          • +
                          +

                          3 - Delegators:

                          +
                            +
                          • If (q1_1 in [3,4,5] and q1_2 in [3,4,5] and q1_3 in [1,2])
                          • +
                          • Or (q1_1 in [3,4,5] and q1_2 in [1,2] and q1_3 in [1,2])
                          • +
                          • Or (q1_1 in [1,2] and q1_2 in [3,4,5] and q1_3 in [1,2])
                          • +
                          +

                          4 - Avoiders:

                          +
                            +
                          • If (q1_1 in [3,4,5] and q1_2 in [3,4,5] and q1_3 in [3,4,5])
                          • +
                          • Or (q1_1 in [3,4,5] and q1_2 in [1,2] and q1_3 in [3,4,5])
                          • +
                          • Or (q1_1 in [1,2] and q1_2 in [3,4,5] and q1_3 in [3,4,5])
                          • +
                          +

                          5 - Others:

                          +
                            +
                          • Everyone else.
                          • +
                          +

                          Create the new metadata:

                          +
                          >>> meta['columns']['segments'] = {
                          +...     'type': 'single',
                          +...     'text': {'en-GB': 'Segments'},
                          +...     'values': [
                          +...         {'value': 1, 'text': {'en-GB': 'Self-directed'}},
                          +...         {'value': 2, 'text': {'en-GB': 'Validators'}},
                          +...         {'value': 3, 'text': {'en-GB': 'Delegators'}},
                          +...         {'value': 4, 'text': {'en-GB': 'Avoiders'}},
                          +...         {'value': 5, 'text': {'en-GB': 'Other'}},
                          +...     ]
                          +... }
                          +
                          +
                          +

                          Initialize the new column?

                          +
                          >>> data['segments'] = np.NaN
                          +
                          +
                          +

                          Create the mapper separately, since it’s pretty massive!

                          +

                          See the Complex logic section for more information and examples +related to the use of union and intersection.

                          +
                          >>> mapper = {
                          +...     1: intersection([
                          +...         {"q1_1": [1, 2]},
                          +...         {"q1_2": [1, 2]},
                          +...         {"q1_3": [3, 4, 5]}
                          +...     ]),
                          +...     2: intersection([
                          +...         {"q1_1": [1, 2]},
                          +...         {"q1_2": [1, 2]},
                          +...         {"q1_3": [1, 2]}
                          +...     ]),
                          +...     3: union([
                          +...         intersection([
                          +...             {"q1_1": [3, 4, 5]},
                          +...             {"q1_2": [3, 4, 5]},
                          +...             {"q1_3": [1, 2]}
                          +...         ]),
                          +...         intersection([
                          +...             {"q1_1": [3, 4, 5]},
                          +...             {"q1_2": [1, 2]},
                          +...             {"q1_3": [1, 2]}
                          +...         ]),
                          +...         intersection([
                          +...             {"q1_1": [1, 2]},
                          +...             {"q1_2": [3, 4, 5]},
                          +...             {"q1_3": [1, 2]}
                          +...         ]),
                          +...     ]),
                          +...     4: union([
                          +...         intersection([
                          +...             {"q1_1": [3, 4, 5]},
                          +...             {"q1_2": [3, 4, 5]},
                          +...             {"q1_3": [3, 4, 5]}
                          +...         ]),
                          +...         intersection([
                          +...             {"q1_1": [3, 4, 5]},
                          +...             {"q1_2": [1, 2]},
                          +...             {"q1_3": [3, 4, 5]}
                          +...         ]),
                          +...         intersection([
                          +...             {"q1_1": [1, 2]},
                          +...             {"q1_2": [3, 4, 5]},
                          +...             {"q1_3": [3, 4, 5]}
                          +...         ])
                          +...     ])
                          +... }
                          +
                          +
                          +

                          Recode the new column:

                          +
                          >>> data['segments'] = recode(
                          +...     meta, data,
                          +...     target='segments',
                          +...     mapper=mapper
                          +... )
                          +
                          +
                          +
                          +

                          Note

                          +

                          Anything not at the top level of the mapper will not benefit from using +the default parameter of the recode function. In this case, for example, +saying default='q1_1' would not have helped. Everything in a nested level +of the mapper, including anything in a union or intersection list, +must use the explicit dict form {"q1_1": [1, 2]}.

                          +
                          +

                          Fill all cases that are still empty with the value 5:

                          +
                          >>> data['segments'].fillna(5, inplace=True)
                          +
                          +
                          +

                          Check the result:

                          +
                          >>> data[['q1_1', 'q1_2', 'q1_3', 'segments']].head(20)
                          +    q1_1  q1_2  q1_3  segments
                          +0      3     3     3         4
                          +1      3     3     3         4
                          +2      1     1     3         1
                          +3      1     1     2         2
                          +4      2     2     2         2
                          +5      1     1     5         1
                          +6      2     3     2         3
                          +7      2     2     3         1
                          +8      1     1     4         1
                          +9      3     3     3         4
                          +10     3     3     4         4
                          +11     2     2     4         1
                          +12     1     1     5         1
                          +13     2     2     4         1
                          +14     1     1     1         2
                          +15     2     2     4         1
                          +16     2     2     3         1
                          +17     1     1     5         1
                          +18     5     5     1         3
                          +19     1     1     4         1
                          +
                          +
                          +
                          +
                          +

                          Variable creation

                          +
                          +
                          +

                          Adding derived variables

                          +
                          +
                          +

                          Interlocking variables

                          +
                          +
                          +

                          Condition-based code removal

                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/engine/00_overview.html b/docs/API/_build/html/sites/lib_doc/engine/00_overview.html new file mode 100644 index 000000000..6c520c338 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/engine/00_overview.html @@ -0,0 +1,449 @@ + + + + + + + + + + + Analysis & aggregation — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          + + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/engine/01_links_stacks.html b/docs/API/_build/html/sites/lib_doc/engine/01_links_stacks.html new file mode 100644 index 000000000..fef5dbbc8 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/engine/01_links_stacks.html @@ -0,0 +1,553 @@ + + + + + + + + + + + Collecting aggregations — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Collecting aggregations

                          +

                          All computational results are collected in a so-called qp.Stack object which +acts as a container for large amount of aggregations in form of qp.Links.

                          + +
                          +

                          Populating a qp.Stack

                          +

                          A qp.Stack is able to cope with a large amount of aggregations, so it is +impractical to add Links one by one with repeated Stack.add_link() calls. +It is much easier to create a “construction plan” using a qp.Batch and +apply the settings saved in DataSet._meta['sets']['batches'] to populate a +qp.Stack instance. In the following, let’s assume dataset is containing +the definitions of two qp.Batches, a qp.Stack can be created running:

                          +
                          stack = dataset.populate(batches='all')
                          +
                          +
                          +

                          For the Batch definitions from here, you +will get the following construction plans:

                          +
                          >>> batch1 = dataset.get_batch('batch1')
                          +>>> batch1.add_y_on_y('y_keys')
                          +
                          +
                          +
                          >>> print batch1.x_y_map
                          +OrderedDict([('q1', ['@', 'gender', 'q1', 'locality', 'ethnicity']),
                          +             ('q2', ['locality', 'ethnicity']),
                          +             ('q6', ['@']),
                          +             ('@', ['q6']),
                          +             (u'q6_1', ['@', 'gender', 'q1']),
                          +             (u'q6_2', ['@', 'gender', 'q1']),
                          +             (u'q6_3', ['@', 'gender', 'q1'])])
                          +
                          +
                          +
                          >>> print batch1.x_filter_map
                          +OrderedDict([('q1', {'(men only)+(q1)': (<function _intersection at 0x0000000019AE06D8>, [{'gender': 1}, {'age': [20, 21, 22, 23, 24, 25]}])}),
                          +             ('q2', {'men only': {'gender': 1}}),
                          +             ('q6', {'men only': {'gender': 1}}),
                          +             ('q6_1', {'men only': {'gender': 1}}),
                          +             ('q6_2', {'men only': {'gender': 1}}),
                          +             ('q6_3', {'men only': {'gender': 1}})])
                          +
                          +
                          +
                          >>> batch2 = dataset.get_batch('batch2')
                          +
                          +
                          +
                          >>> print batch2.x_y_map
                          +OrderedDict([('q2b', ['@', 'gender'])])
                          +
                          +
                          +
                          >>> print batch2.x_filter_map
                          +OrderedDict([('q2b', 'no_filter')])
                          +
                          +
                          +

                          As both Batches refer to the same data file, the same data_key (in this +case the name of dataset) is defining all Links.

                          +

                          After populating the Stack content can be viewed using .describe():

                          +
                          >>> stack.describe()
                          +                data           filter       x          y  view  #
                          +0   Example Data (A)         men only      q1         q1   NaN  1
                          +1   Example Data (A)         men only      q1          @   NaN  1
                          +2   Example Data (A)         men only      q1     gender   NaN  1
                          +3   Example Data (A)         men only       @         q6   NaN  1
                          +4   Example Data (A)         men only      q2  ethnicity   NaN  1
                          +5   Example Data (A)         men only      q2   locality   NaN  1
                          +6   Example Data (A)         men only    q6_1         q1   NaN  1
                          +7   Example Data (A)         men only    q6_1          @   NaN  1
                          +8   Example Data (A)         men only    q6_1     gender   NaN  1
                          +9   Example Data (A)         men only    q6_2         q1   NaN  1
                          +10  Example Data (A)         men only    q6_2          @   NaN  1
                          +11  Example Data (A)         men only    q6_2     gender   NaN  1
                          +12  Example Data (A)         men only    q6_3         q1   NaN  1
                          +13  Example Data (A)         men only    q6_3          @   NaN  1
                          +14  Example Data (A)         men only    q6_3     gender   NaN  1
                          +15  Example Data (A)         men only  gender         q1   NaN  1
                          +16  Example Data (A)         men only  gender          @   NaN  1
                          +17  Example Data (A)         men only  gender     gender   NaN  1
                          +18  Example Data (A)         men only      q6          @   NaN  1
                          +19  Example Data (A)  (men only)+(q1)      q1         q1   NaN  1
                          +20  Example Data (A)  (men only)+(q1)      q1          @   NaN  1
                          +21  Example Data (A)  (men only)+(q1)      q1   locality   NaN  1
                          +22  Example Data (A)  (men only)+(q1)      q1  ethnicity   NaN  1
                          +23  Example Data (A)  (men only)+(q1)      q1     gender   NaN  1
                          +24  Example Data (A)        no_filter     q2b          @   NaN  1
                          +25  Example Data (A)        no_filter     q2b     gender   NaN  1
                          +
                          +
                          +

                          You can find all combinations defined in the x_y_map in the +Stack structure, but also Links like Stack['Example Data (A)']['men only']['gender']['gender'] +are included. These special cases arising from the y_on_y setting. Sometimes +it is helpful to group a describe-dataframe and create a cross-tabulation +of the four Link attributes to get a better overview, e.g. to see how many +Links are included for each x-filter combination. +:

                          +
                          >>> stack.describe('x', 'filter')
                          +filter  (men only)+(q1)  men only  no_filter
                          +x
                          +@                   NaN       1.0        NaN
                          +gender              NaN       3.0        NaN
                          +q1                  5.0       3.0        NaN
                          +q2                  NaN       2.0        NaN
                          +q2b                 NaN       NaN        2.0
                          +q6                  NaN       1.0        NaN
                          +q6_1                NaN       3.0        NaN
                          +q6_2                NaN       3.0        NaN
                          +q6_3                NaN       3.0        NaN
                          +
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/engine/02_quantity.html b/docs/API/_build/html/sites/lib_doc/engine/02_quantity.html new file mode 100644 index 000000000..38764cd4d --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/engine/02_quantity.html @@ -0,0 +1,430 @@ + + + + + + + + + + + The computational engine — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          The computational engine

                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/engine/03_test.html b/docs/API/_build/html/sites/lib_doc/engine/03_test.html new file mode 100644 index 000000000..796d44d54 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/engine/03_test.html @@ -0,0 +1,430 @@ + + + + + + + + + + + Significance testing — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Significance testing

                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/engine/04_agg_methods.html b/docs/API/_build/html/sites/lib_doc/engine/04_agg_methods.html new file mode 100644 index 000000000..f5acf37d1 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/engine/04_agg_methods.html @@ -0,0 +1,915 @@ + + + + + + + + + + + View aggregation — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          View aggregation

                          +

                          All following examples are working with a qp.Stack that was populated +from a qp.DataSet including the following qp.Batch definitions:

                          +
                          >>> batch1 = dataset.get_batch('batch1')
                          +>>> batch1.add_y_on_y('y_keys')
                          +
                          +
                          +
                          >>> print batch1.x_y_map
                          +OrderedDict([('q1', ['@', 'gender', 'q1', 'locality', 'ethnicity']),
                          +             ('q2', ['locality', 'ethnicity']),
                          +             ('q6', ['@']),
                          +             ('@', ['q6']),
                          +             (u'q6_1', ['@', 'gender', 'q1']),
                          +             (u'q6_2', ['@', 'gender', 'q1']),
                          +             (u'q6_3', ['@', 'gender', 'q1'])])
                          +
                          +
                          +
                          >>> print batch1.x_filter_map
                          +OrderedDict([('q1', {'(men only)+(q1)': (<function _intersection at 0x0000000019AE06D8>, [{'gender': 1}, {'age': [20, 21, 22, 23, 24, 25]}])}),
                          +             ('q2', {'men only': {'gender': 1}}),
                          +             ('q6', {'men only': {'gender': 1}}),
                          +             ('q6_1', {'men only': {'gender': 1}}),
                          +             ('q6_2', {'men only': {'gender': 1}}),
                          +             ('q6_3', {'men only': {'gender': 1}})])
                          +
                          +
                          +
                          >>> print batch1.weights
                          +['weight_a']
                          +
                          +
                          +
                          >>> batch2 = dataset.get_batch('batch2')
                          +
                          +
                          +
                          >>> print batch2.x_y_map
                          +OrderedDict([('q2b', ['@', 'gender'])])
                          +
                          +
                          +
                          >>> print batch2.x_filter_map
                          +OrderedDict([('q2b', 'no_filter')])
                          +
                          +
                          +
                          >>> print batch2.weights
                          +['weight']
                          +
                          +
                          +
                          +

                          Basic views

                          +

                          It is possible to add various qp.Views to a Link. This can be performed +by running Stack.add_link() providing View objects via the view parameter. +Alternatively, the qp.Batch definitions that are stored in the meta data +help to add basic Views (counts, percentages, bases and sums). By simply +running Stack.aggregate() we can easily add a large amount of aggregations +in one step.

                          +
                          +

                          Note

                          +

                          Stack.aggregate() can only be used with pre-populated Stacks! +(see DataSet.populate()).

                          +
                          +

                          For instance, we can add column percentages and (unweighted and weighted) base sizes +to all Links of batch2 like this:

                          +
                          >>> stack.aggregate(views=['c%', 'cbase'], unweighted_base=True, batches='batch2', verbose=False)
                          +>>> stack.describe()
                          +                data           filter       x          y                  view  #
                          +0   Example Data (A)         men only      q1         q1                   NaN  1
                          +1   Example Data (A)         men only      q1          @                   NaN  1
                          +2   Example Data (A)         men only      q1     gender                   NaN  1
                          +3   Example Data (A)         men only       @         q6                   NaN  1
                          +4   Example Data (A)         men only      q2  ethnicity                   NaN  1
                          +5   Example Data (A)         men only      q2   locality                   NaN  1
                          +6   Example Data (A)         men only    q6_1         q1                   NaN  1
                          +7   Example Data (A)         men only    q6_1          @                   NaN  1
                          +8   Example Data (A)         men only    q6_1     gender                   NaN  1
                          +9   Example Data (A)         men only    q6_2         q1                   NaN  1
                          +10  Example Data (A)         men only    q6_2          @                   NaN  1
                          +11  Example Data (A)         men only    q6_2     gender                   NaN  1
                          +12  Example Data (A)         men only    q6_3         q1                   NaN  1
                          +13  Example Data (A)         men only    q6_3          @                   NaN  1
                          +14  Example Data (A)         men only    q6_3     gender                   NaN  1
                          +15  Example Data (A)         men only  gender         q1                   NaN  1
                          +16  Example Data (A)         men only  gender          @                   NaN  1
                          +17  Example Data (A)         men only  gender     gender                   NaN  1
                          +18  Example Data (A)         men only      q6          @                   NaN  1
                          +19  Example Data (A)  (men only)+(q1)      q1         q1                   NaN  1
                          +20  Example Data (A)  (men only)+(q1)      q1          @                   NaN  1
                          +21  Example Data (A)  (men only)+(q1)      q1   locality                   NaN  1
                          +22  Example Data (A)  (men only)+(q1)      q1  ethnicity                   NaN  1
                          +23  Example Data (A)  (men only)+(q1)      q1     gender                   NaN  1
                          +24  Example Data (A)        no_filter     q2b          @     x|f|:|y|weight|c%  1
                          +25  Example Data (A)        no_filter     q2b          @  x|f|x:||weight|cbase  1
                          +26  Example Data (A)        no_filter     q2b          @        x|f|x:|||cbase  1
                          +27  Example Data (A)        no_filter     q2b     gender     x|f|:|y|weight|c%  1
                          +28  Example Data (A)        no_filter     q2b     gender  x|f|x:||weight|cbase  1
                          +29  Example Data (A)        no_filter     q2b     gender        x|f|x:|||cbase  1
                          +
                          +
                          +

                          Obviously Views are only added to Links defined by batch2 and +automatically weighted according to the weight definition of batch2, +which is evident from the view keys (x|f|:|y|weight|c%). Combining the information +of the four Link attributes with a view key, leads to a pd.DataFrame +and its belonging meta information:

                          +
                          >>> link = stack['Example Data (A)']['no_filter']['q2b']['gender']
                          +>>> view_key = 'x|f|:|y|weight|c%'
                          +
                          +
                          +
                          >>> link[view_key]
                          +Question               q2b
                          +Values                   @
                          +Question Values
                          +q2b      1       11.992144
                          +         2       80.802580
                          +         3        7.205276
                          +
                          +
                          +
                          >>> link[view_key].meta()
                          +{
                          +    "agg": {
                          +        "weights": "weight",
                          +        "name": "c%",
                          +        "grp_text_map": null,
                          +        "text": "",
                          +        "fullname": "x|f|:|y|weight|c%",
                          +        "is_weighted": true,
                          +        "method": "frequency",
                          +        "is_block": false
                          +    },
                          +    "x": {
                          +        "is_array": false,
                          +        "name": "q2b",
                          +        "is_multi": false,
                          +        "is_nested": false
                          +    },
                          +    "shape": [
                          +        3,
                          +        1
                          +    ],
                          +    "y": {
                          +        "is_array": false,
                          +        "name": "@",
                          +        "is_multi": false,
                          +        "is_nested": false
                          +    }
                          +}
                          +
                          +
                          +

                          Now we are adding Views to all batch1-defined Links as well:

                          +
                          >>> stack.aggregate(views=['c%', 'counts', 'cbase'], unweighted_base=True, batches='batch1', verbose=False)
                          +>>> stack.describe(['x', 'view'], 'y').loc[['@', 'q6'], ['@', 'q6']]
                          +y                            @   q6
                          +x  view
                          +@  x|f|:|y|weight_a|c%     NaN  1.0
                          +   x|f|:||weight_a|counts  NaN  1.0
                          +q6 x|f|:|y|weight_a|c%     1.0  NaN
                          +   x|f|:||weight_a|counts  1.0  NaN
                          +
                          +
                          +

                          Even if unweighted bases are requested, they get skipped for array summaries +and transposed arrays.

                          +

                          Since y_on_y is requested, for a variable used as cross- and downbreak, with an extended filter (in this +example q1), two Links with Views are created:

                          +
                          >>> stack.describe(['y', 'filter', 'view'], 'x').loc['q1', 'q1']
                          +filter           view
                          +(men only)+(q1)  x|f|:|y|weight_a|c%       1.0
                          +                 x|f|:||weight_a|counts    1.0
                          +                 x|f|x:||weight_a|cbase    1.0
                          +                 x|f|x:|||cbase            1.0
                          +men only         x|f|:|y|weight_a|c%       1.0
                          +                 x|f|:||weight_a|counts    1.0
                          +                 x|f|x:||weight_a|cbase    1.0
                          +                 x|f|x:|||cbase            1.0
                          +
                          +
                          +

                          The first one is the aggregation defined by the Batch construction plan, +the second one shows the y_on_y aggregation using only the main +Batch.filter.

                          +
                          +
                          +

                          Non-categorical variables

                          +
                          >>> batch3 = dataset.add_batch('batch3')
                          +>>> batch3.add_x('age')
                          +>>> stack = dataset.populate('batch3')
                          +>>> stack.describe()
                          +               data     filter    x  y  view  #
                          +0  Example Data (A)  no_filter  age  @   NaN  1
                          +
                          +
                          +

                          Non-categorical variables (ìnt or float) are handled in a special way. +There are two options:

                          +
                          +
                            +
                          • Treat them like categorical variables: +Append them to the parameter categorize, then counts, percentage +and sum aggregations can be added alongside the cbase View.

                            +
                            >>> stack.aggregate(views=['c%', 'counts', 'cbase', 'counts_sum', 'c%_sum'],
                            +                    unweighted_base=True,
                            +                    categorize=['age'],
                            +                    batches='batch3',
                            +                    verbose=False)
                            +
                            +
                            +
                            >>> stack.describe()
                            +               data     filter    x  y                     view  #
                            +0  Example Data (A)  no_filter  age  @           x|f|:|||counts  1
                            +1  Example Data (A)  no_filter  age  @     x|f.c:f|x:|y||c%_sum  1
                            +2  Example Data (A)  no_filter  age  @              x|f|:|y||c%  1
                            +3  Example Data (A)  no_filter  age  @           x|f|x:|||cbase  1
                            +4  Example Data (A)  no_filter  age  @  x|f.c:f|x:|||counts_sum  1
                            +
                            +
                            +
                          • +
                          • Do not categorize the variable: +Only cbase is created and additional descriptive statistics +Views must be added. The method will raise a warning:

                            +
                            >>> stack.aggregate(views=['c%', 'counts', 'cbase', 'counts_sum', 'c%_sum'],
                            +                    unweighted_base=True,
                            +                    batches='batch3',
                            +                    verbose=True)
                            +Warning: Found 1 non-categorized numeric variable(s): ['age'].
                            +Descriptive statistics must be added!
                            +
                            +
                            +
                            >>> stack.describe()
                            +               data     filter    x  y            view  #
                            +0  Example Data (A)  no_filter  age  @  x|f|x:|||cbase  1
                            +
                            +
                            +
                          • +
                          +
                          +
                          +
                          +

                          Descriptive statistics

                          +
                          >>> b_name = 'batch4'
                          +>>> batch4 = dataset.add_batch(b_name)
                          +>>> batch4.add_x(['q2b', 'q6', 'age'])
                          +>>> stack = dataset.populate(b_name)
                          +>>> stack.aggregate(views=['counts', 'cbase'], batches=b_name, verbose=False)
                          +
                          +
                          +
                          >>> stack.describe()
                          +                data     filter     x  y            view  #
                          +0   Example Data (A)  no_filter   q2b  @  x|f|:|||counts  1
                          +1   Example Data (A)  no_filter   q2b  @  x|f|x:|||cbase  1
                          +2   Example Data (A)  no_filter  q6_1  @  x|f|:|||counts  1
                          +3   Example Data (A)  no_filter  q6_1  @  x|f|x:|||cbase  1
                          +4   Example Data (A)  no_filter  q6_2  @  x|f|:|||counts  1
                          +5   Example Data (A)  no_filter  q6_2  @  x|f|x:|||cbase  1
                          +6   Example Data (A)  no_filter  q6_3  @  x|f|:|||counts  1
                          +7   Example Data (A)  no_filter  q6_3  @  x|f|x:|||cbase  1
                          +8   Example Data (A)  no_filter   age  @  x|f|x:|||cbase  1
                          +9   Example Data (A)  no_filter    q6  @  x|f|:|||counts  1
                          +10  Example Data (A)  no_filter    q6  @  x|f|x:|||cbase  1
                          +
                          +
                          +

                          Adding descriptive statistics Views like mean, stddev, min, max, median, etc. +can be added with the method stack.add_stats(). With the parameters +other_source, rescale and exclude you can specify the calculation. +Again each combination of the parameters refers to a unique view key. Note that +in on_vars included arrays get unrolled, that means also all belonging +array items get equipped with the added View:

                          +
                          >>> stack.add_stats(on_vars=['q2b', 'age'], stats='mean', _batches=b_name, verbose=False)
                          +>>> stack.add_stats(on_vars=['q6'], stats='stddev', _batches=b_name, verbose=False)
                          +>>> stack.add_stats(on_vars=['q2b'], stats='mean', rescale={1:100, 2:50, 3:0},
                          +...                 custom_text='rescale mean', _batches=b_name, verbose=False)
                          +
                          +
                          +
                          >>> stack.describe('view', 'x')
                          +x                               age  q2b   q6  q6_1  q6_2  q6_3
                          +view
                          +x|d.mean|x:|||stat              1.0  1.0  NaN   NaN   NaN   NaN
                          +x|d.mean|x[{100,50,0}]:|||stat  NaN  1.0  NaN   NaN   NaN   NaN
                          +x|d.stddev|x:|||stat            NaN  NaN  1.0   1.0   1.0   1.0
                          +x|f|:|||counts                  NaN  1.0  1.0   1.0   1.0   1.0
                          +x|f|x:|||cbase                  1.0  1.0  1.0   1.0   1.0   1.0
                          +
                          +
                          +
                          +
                          +

                          Nets

                          +
                          >>> b_name = 'batch5'
                          +>>> batch5 = dataset.add_batch(b_name)
                          +>>> batch5.add_x(['q2b', 'q6'])
                          +>>> stack = dataset.populate(b_name)
                          +>>> stack.aggregate(views=['counts', 'c%', 'cbase'], batches=b_name, verbose=False)
                          +
                          +
                          +
                          >>> stack.describe('view', 'x')
                          +x               q2b  q6  q6_1  q6_2  q6_3
                          +view
                          +x|f|:|y||c%       1   1     1     1     1
                          +x|f|:|||counts    1   1     1     1     1
                          +x|f|x:|||cbase    1   1     1     1     1
                          +
                          +
                          +

                          Net-like Views can be added with the method Stack.add_nets() by defining +net_maps for selected variables. There is a distinction between two different +types of net Views:

                          +
                          +
                            +
                          • Expanded nets: +The existing counts or percentage Views are replaced with the new +net Views, which will the net-defining codes after or before the +computed net groups (i.e. “overcode” nets).

                            +
                            >>> stack.add_nets('q2b', [{'Top2': [1, 2]}], expand='after', _batches=b_name, verbose=False)
                            +
                            +
                            +
                            >>> stack.describe('view', 'x')
                            +x                       q2b   q6  q6_1  q6_2  q6_3
                            +view
                            +x|f|:|y||c%             NaN  1.0   1.0   1.0   1.0
                            +x|f|:|||counts          NaN  1.0   1.0   1.0   1.0
                            +x|f|x:|||cbase          1.0  1.0   1.0   1.0   1.0
                            +x|f|x[{1,2}+]*:|y||net  1.0  NaN   NaN   NaN   NaN
                            +x|f|x[{1,2}+]*:|||net   1.0  NaN   NaN   NaN   NaN
                            +
                            +
                            +
                          • +
                          • Not expanded nets: +The new net Views are added to the stack, which contain only the +computed net groups.

                            +
                            >>> stack.add_nets('q2b', [{'Top2': [1, 2]}], _batches=b_name, verbose=False)
                            +
                            +
                            +
                            >>> stack.describe('view', 'x')
                            +x                       q2b   q6  q6_1  q6_2  q6_3
                            +view
                            +x|f|:|y||c%             NaN  1.0   1.0   1.0   1.0
                            +x|f|:|||counts          NaN  1.0   1.0   1.0   1.0
                            +x|f|x:|||cbase          1.0  1.0   1.0   1.0   1.0
                            +x|f|x[{1,2}+]*:|y||net  1.0  NaN   NaN   NaN   NaN
                            +x|f|x[{1,2}+]*:|||net   1.0  NaN   NaN   NaN   NaN
                            +x|f|x[{1,2}]:|y||net    1.0  NaN   NaN   NaN   NaN
                            +x|f|x[{1,2}]:|||net     1.0  NaN   NaN   NaN   NaN
                            +
                            +
                            +
                          • +
                          +
                          +

                          The difference between the two net types are also visible in the view keys: +x|f|x[{1,2}+]*:|||net versus x|f|x[{1,2}]:|||net.

                          +
                          +

                          Net definitions

                          +

                          To create more complex net definitions the method quantipy.net() can be used, +which generates a well-formatted instruction dict and appends it to the net_map. +It’s a helper especially concerning including various texts with different +valid text_keys. The next example shows how to prepare a net for ‘q6’ +(promoters, detractors):

                          +
                          >>> q6_net = qp.net([], [1, 2, 3, 4, 5, 6], 'Promotors', ['en-GB', 'sv-SE'])
                          +>>> q6_net = qp.net(q6_net, [9, 10], {'en-GB': 'Detractors',
                          +...                                   'sv_SE': 'Detractors',
                          +...                                   'de-DE': 'Kritiker'})
                          +>>> qp.net(q6_net[0], text='Promoter', text_key='de-DE')
                          +
                          +
                          +
                          >>> print q6_net
                          +[
                          +    {
                          +        "1": [1, 2, 3, 4, 5, 6],
                          +        "text": {
                          +            "en-GB": "Promotors",
                          +            "sv-SE": "Promotors",
                          +            "de-DE": "Promoter"
                          +        }
                          +    },
                          +    {
                          +        "2": [9, 10],
                          +        "text": {
                          +            "en-GB": "Detractors",
                          +            "sv_SE": "Detractors",
                          +            "de-DE": "Kritiker"
                          +        }
                          +    }
                          +]
                          +
                          +
                          +
                          +
                          +

                          Calculations

                          +

                          Stack.add_nets() has the parameter calc, which allows adding Views +that are calculated out of the defined nets. The method qp.calc() is a +helper to create a well-formatted instruction dict for the calculation. +For instance, to calculate the NPS (promoters - detractors) for 'q6', see the example +above and create the following calculation:

                          +
                          >>> q6_calc = qp.calc((1, '-', 2), 'NPS', ['en-GB', 'sv-SE', 'de-DE'])
                          +
                          +
                          +
                          >>> print q6_calc
                          +OrderedDict([('calc', ('net_1', <built-in function sub>, 'net_2')),
                          +            ('calc_only', False),
                          +            ('text', {'en-GB': 'NPS',
                          +                      'sv-SE': 'NPS',
                          +                      'de-DE': 'NPS'})])
                          +
                          +
                          +
                          >>> stack.add_nets('q6', q6_net, calc=q6_calc, _batches=b_name, verbose=False)
                          +
                          +
                          +
                          >>> stack.describe('view', 'x')
                          +x                                                   q2b   q6  q6_1  q6_2  q6_3
                          +view
                          +x|f.c:f|x[{1,2,3,4,5,6}],x[{9,10}],x[{1,2,3,4,5...  NaN  1.0   1.0   1.0   1.0
                          +x|f.c:f|x[{1,2,3,4,5,6}],x[{9,10}],x[{1,2,3,4,5...  NaN  1.0   1.0   1.0   1.0
                          +x|f|:|y||c%                                         NaN  1.0   1.0   1.0   1.0
                          +x|f|:|||counts                                      NaN  1.0   1.0   1.0   1.0
                          +x|f|x:|||cbase                                      1.0  1.0   1.0   1.0   1.0
                          +x|f|x[{1,2}+]*:|y||net                              1.0  NaN   NaN   NaN   NaN
                          +x|f|x[{1,2}+]*:|||net                               1.0  NaN   NaN   NaN   NaN
                          +x|f|x[{1,2}]:|y||net                                1.0  NaN   NaN   NaN   NaN
                          +x|f|x[{1,2}]:|||net                                 1.0  NaN   NaN   NaN   NaN
                          +
                          +
                          +

                          You can see that nets that are added on arrays are also applied for all array items.

                          +
                          +
                          +
                          +

                          Cumulative sums

                          +

                          Cumulative sum Views can be added to a specified collection of xks of the +Stack using stack.cumulative_sum(). These Views will always +replace the regular counts and percentage Views:

                          +
                          >>> b_name = 'batch6'
                          +>>> batch6 = dataset.add_batch(b_name)
                          +>>> batch6.add_x(['q2b', 'q6'])
                          +>>> stack = dataset.populate(b_name)
                          +>>> stack.aggregate(views=['counts', 'c%', 'cbase'], batches=b_name, verbose=False)
                          +
                          +
                          +
                          >>> stack.cumulative_sum('q6', verbose=False)
                          +
                          +
                          +
                          >>> stack.describe('view', 'x')
                          +x                             q2b   q6  q6_1  q6_2  q6_3
                          +view
                          +x|f.c:f|x++:|y||c%_cumsum     NaN  1.0   1.0   1.0   1.0
                          +x|f.c:f|x++:|||counts_cumsum  NaN  1.0   1.0   1.0   1.0
                          +x|f|:|y||c%                   1.0  NaN   NaN   NaN   NaN
                          +x|f|:|||counts                1.0  NaN   NaN   NaN   NaN
                          +x|f|x:|||cbase                1.0  1.0   1.0   1.0   1.0
                          +
                          +
                          +
                          +
                          +

                          Significance tests

                          +
                          >>> batch2 = dataset.get_batch('batch2')
                          +>>> batch2.set_sigtests([0.05])
                          +>>> batch5 = dataset.get_batch('batch5')
                          +>>> batch5.set_sigtests([0.01, 0.05])
                          +>>> stack = dataset.populate(['batch2', 'batch5'])
                          +
                          +
                          +
                          >>> stack.aggregate(['counts', 'cbase'], batches=['batch2', 'batch5'], verbose=False)
                          +
                          +
                          +
                          >>> stack.describe(['view', 'y'], 'x')
                          +x                            q2b   q6  q6_1  q6_2  q6_3
                          +view                 y
                          +x|f|:||weight|counts @       1.0  NaN   NaN   NaN   NaN
                          +                     gender  1.0  NaN   NaN   NaN   NaN
                          +x|f|:|||counts       @       1.0  1.0   1.0   1.0   1.0
                          +x|f|x:||weight|cbase @       1.0  NaN   NaN   NaN   NaN
                          +                     gender  1.0  NaN   NaN   NaN   NaN
                          +x|f|x:|||cbase       @       1.0  1.0   1.0   1.0   1.0
                          +                     gender  1.0  NaN   NaN   NaN   NaN
                          +
                          +
                          +

                          Significance tests can only be added Batch-wise, which also means that +significance levels must be defined for each Batch before running +stack.add_tests().

                          +
                          >>> stack.add_tests(['batch2', 'batch5'], verbose=False)
                          +
                          +
                          +
                          >>> stack.describe(['view', 'y'], 'x')
                          +x                                               q2b   q6  q6_1  q6_2  q6_3
                          +view                                    y
                          +x|f|:||weight|counts                    @       1.0  NaN   NaN   NaN   NaN
                          +                                        gender  1.0  NaN   NaN   NaN   NaN
                          +x|f|:|||counts                          @       1.0  1.0   1.0   1.0   1.0
                          +x|f|x:||weight|cbase                    @       1.0  NaN   NaN   NaN   NaN
                          +                                        gender  1.0  NaN   NaN   NaN   NaN
                          +x|f|x:|||cbase                          @       1.0  1.0   1.0   1.0   1.0
                          +                                        gender  1.0  NaN   NaN   NaN   NaN
                          +x|t.props.Dim.01|:|||significance       @       1.0  NaN   1.0   1.0   1.0
                          +x|t.props.Dim.05|:||weight|significance @       1.0  NaN   NaN   NaN   NaN
                          +                                        gender  1.0  NaN   NaN   NaN   NaN
                          +x|t.props.Dim.05|:|||significance       @       1.0  NaN   1.0   1.0   1.0
                          +
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/lib_doc/overview.html b/docs/API/_build/html/sites/lib_doc/overview.html new file mode 100644 index 000000000..7490bfe88 --- /dev/null +++ b/docs/API/_build/html/sites/lib_doc/overview.html @@ -0,0 +1,417 @@ + + + + + + + + + + + Documentation — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +

                          Documentation

                          +
                          +
                          +
                          + + +
                          +
                          +
                          + + +
                          + +
                          +

                          + © Copyright 2017, Quantipy dev team. + +

                          +
                          + Built with Sphinx using a theme provided by Read the Docs. + +
                          + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/00_overview.html b/docs/API/_build/html/sites/release_notes/00_overview.html new file mode 100644 index 000000000..696833669 --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/00_overview.html @@ -0,0 +1,463 @@ + + + + + + + + + + + Release notes — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + + + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/01_latest.html b/docs/API/_build/html/sites/release_notes/01_latest.html new file mode 100644 index 000000000..57f70ecda --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/01_latest.html @@ -0,0 +1,555 @@ + + + + + + + + + + + Upcoming (September) — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Upcoming (September)

                          +

                          New: DataSet.min_value_count()

                          +

                          A new wrapper for DataSet.hiding() is included. All values are hidden, +that have less counts than the included number min. +The used data can be weighted or filtered using the parameters weight and +condition.

                          +

                          Usage as Batch method: +Batch.min_value_count() without the parameters weight and +condition automatically grabs Batch.weights[0] and Batch.filter +to calculate low value counts.

                          +
                          +

                          New: Prevent weak duplicated in data

                          +

                          As Python is case sensitive it is possible to have two or more variables with +the same name, but in lower- and uppercases. Most other software do not support +that, so a warning is shown if a weak dupe is created. Additionally +Dataset.write_dimensions() performs auto-renaming is weak dupes are detected.

                          +
                          +

                          New: Prevent single-cat delimited sets

                          +

                          DataSet.add_meta(..., qtype='delimited set', categories=[...], ...) +automatically switches qtype to single if only one category is defined. +DataSet.convert(name, 'single') allows conversion from delimited set to +single if the variable has only one category. +DataSet.repair() and DataSt.remove_values() convert delimited sets +automatically to singles if only one category is included.

                          +
                          +

                          Update: merge warnings + merging delimites sets

                          +

                          Warnings in hmerge() and vmerge() are updated. If a column exists in +the left and the right dataset, the type is compared. Some type inconsistencies +are allowed, but return a warning, while others end up in a raise.

                          +

                          delimited sets in vmerge():

                          +

                          If a column is a delimited set in the left dataset, but a single, int or float +in the right dataset, the data of the right column is converted into a delimited +set.

                          +

                          delimited sets in hmerge(...merge_existing=None):

                          +

                          For the hmerge a new parameter merge_existing is included, which can be +None, a list of variable-names or 'all'.

                          +

                          If delimited sets are included in left and right dataset:

                          +
                            +
                          • merge_existing=None: Only meta is adjusted. Data is untouched (left data
                          • +
                          +

                          is taken). +* merge_existing='all': Meta and data are merged for all delimited sets, +that are included in both datasets. +* merge_existing=[variable-names]: Meta and data are merged for all +delimited sets, that are listed and included in both datasets.

                          +
                          +

                          Update: encoding in DataSet.get_batch(name)

                          +

                          The method is not that encoding sensitive anymore. It returns the depending +Batch, no matter if '...', u'...' or '...'.decode('utf8') is +included as name.

                          +
                          +

                          Update: warning in weight engine

                          +

                          Missing codes in the sample are only alerted, if the belonging target is not 0.

                          +
                          +

                          Update: DataSet.to_array(..., variables, ...)

                          +

                          Duplicated vars in variables are not allowed anymore, these were causing +problems in the ChainManager class.

                          +
                          +

                          Update: Batch.add_open_ends()

                          +

                          Method raises an error if no vars are included in oe and break_by. The +empty dataframe was causing issues in the ChainManager class.

                          +
                          +

                          Update: Batch.extend_x()

                          +

                          The method automatically checks if the included variables are arrays and adds +them to Batch.summaries if they are included yet.

                          +
                          +
                          +
                          +

                          Latest (04/06/2018)

                          +

                          New: Additional variable (names) “getter”-like and resolver methods

                          +
                            +
                          • DataSet.created()
                          • +
                          • DataSet.find(str_tags=None, suffixed=False)
                          • +
                          • DataSet.names()
                          • +
                          • DataSet.resolve_name()
                          • +
                          +

                          A bunch of new methods enhancing the options of finding and testing for variable +names have been added. created() will list all variables that have been added +to a dataset using core functions, i.e. add_meta() and derive(), resp. +all helper methods that use them internally (as band() or categorize() do +for instance).

                          +

                          The find() method is returning all variable names that contain any of the +provided substrings in str_tags. To only consider names that end with these +strings, set suffixed=True. If no str_tags are passed, the method will +use a default list of tags including ['_rc', '_net', ' (categories', ' (NET', '_rec'].

                          +

                          Sometimes a dataset might contain “semi-duplicated” names, variables that differ +in respect to case sensitivity but have otherwise identical names. Calling +names() will report such cases in a pd.DataFrame that lists all name +variants under the respective str.lower() version. If no semi-duplicates +are found, names() will simply return DataSet.variables().

                          +

                          Lastly, resolve_name() can be used to return the “proper”, existing representation(s) of a given variable name’s spelling.

                          +
                          +

                          New: Batch.remove()

                          +

                          Not needed batches can be removed from meta, so they are not aggregated +anymore.

                          +
                          +

                          New: Batch.rename(new_name)

                          +

                          Sometimes standard batches have long/ complex names. They can now be changed +into a custom name. Please take into account, that for most hubs the name of +omnibus batches should look like ‘client ~ topic’.

                          +
                          +

                          Update: Handling verbatims in qp.Batch

                          +

                          Instead of holding the well prepared open-end dataframe in batch.verbatims, +the attribute is now filled by batch.add_open_ends() with instructions to +create the open-end dataframe. It is easier to to modify/ overwrite existing +verbatims. Therefore also a new parameter is included overwrite=True.

                          +
                          +

                          Update: Batch.copy(..., b_filter=None, as_addition=False)

                          +

                          It is now possible to define an additional filter for a copied batch and also +to set it as addition to the master batch.

                          +
                          +

                          Update: Regrouping the variable list using DataSet.order(..., regroup=True)

                          +

                          A new parameter called regroup will instruct reordering all newly created +variables into their logical position of the dataset’s main variable order, i.e. +attempting to place derived variables after the originating ones.

                          +
                          +

                          Bugfix: add_meta() and duplicated categorical values codes

                          +

                          Providing duplicated numerical codes while attempting to create new metadata +using add_meta() will now correctly raise a ValueError to prevent +corrupting the DataSet.

                          +
                          >>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')]
                          +>>> dataset.add_meta('test_var', 'single', 'test label', cats)
                          +ValueError: Cannot resolve category definition due to code duplicates: [2, 3]
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/02_archive.html b/docs/API/_build/html/sites/release_notes/02_archive.html new file mode 100644 index 000000000..653dfb55f --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/02_archive.html @@ -0,0 +1,1513 @@ + + + + + + + + + + + Archived release notes — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Archived release notes

                          +
                          +

                          sd (04/04/2018)

                          +

                          New: Emptiness handlers in DataSet and Batch classes

                          +
                            +
                          • DataSet.empty(name, condition=None)
                          • +
                          • DataSet.empty_items(name, condition=None, by_name=True)
                          • +
                          • DataSet.hide_empty_items(condition=None, arrays=None)
                          • +
                          • Batch.hide_empty(xks=True, summaries=True)
                          • +
                          +

                          empty() is used to test if regular variables are completely empty, +empty_items() checks the same for the items of an array mask definition. +Both can be run on lists of variables. If a single variable is tested, the former +returns simply boolean, the latter will list all empty items. If lists are checked, +empty() returns the sublist of empty variables, empty_items() is mapping +the list of empty items per array name. The condition parameter of these +methods takes a Quantipy logic expression to restrict the test to a subset +of the data, i.e. to check if variables will be empty if the dataset is filtered +a certain way. A very simple example:

                          +
                          >>> dataset.add_meta('test_var', 'int', 'Variable is empty')
                          +>>> dataset.empty('test_var')
                          +True
                          +
                          +
                          +
                          >>> dataset[dataset.take({'gender': 1}), 'test_var'] = 1
                          +>>> dataset.empty('test_var')
                          +False
                          +
                          +
                          +
                          >>> dataset.empty('test_var', {'gender': 2})
                          +True
                          +
                          +
                          +

                          The DataSet method hide_empty_items() uses the emptiness tests to +automatically apply a hiding rule on all empty items found in the dataset. +To restrict this to specific arrays only, their names can be provided via the +arrays argument. Batch.hide_empty() takes into account the current +Batch.filter setup and by drops/hides all relevant empty variables from the +xks list and summary aggregations by default. Summaries that would end up without valid +items because of this are automatically removed from the summaries collection +and the user is warned.

                          +
                          +

                          New: qp.set_option('fast_stack_filters', True)

                          +

                          A new option to enable a more efficient test for already existing filters +inside the qp.Stack object has been added. Set the 'fast_stack_filters' +option to True to use it, the default is False to ensure compatibility +in different versions of production DP template workspaces.

                          +
                          +

                          Update: Stack.add_stats(..., factor_labels=True, ...)

                          +

                          The parameter factor_labels is now also able to take the string '()', +then factors are written in the normal brackets next to the label (instead +of []).

                          +

                          In the new version factor_labels are also just added if there are none included +before, except new scales are used.

                          +
                          +

                          Bugfix: DataSet np.NaN insertion to delimited_set variables

                          +

                          np.NaN was incorrectly transformed when inserted into delimited_set +before, leading to either numpy type conflicts or type casting exceptions. +This is now fixed.

                          +
                          +
                          +

                          sd (27/02/2018)

                          +

                          New: DataSet._dimensions_suffix

                          +

                          DataSet has a new attribute _dimensions_suffix, which is used as mask +suffix while running DataSet.dimensionize(). The default is _grid and +it can be modified with DataSet.set_dim_suffix().

                          +
                          +

                          Update: Stack._get_chain() (old chain)

                          +

                          The method is speeded-up. If a filter is already included in the Stack, it is +not calculated from scratch anymore. Additionally the method has a new parameter +described, which takes a describing dataframe of the Stack, so it no longer +needs to be calculated in each loop.

                          +

                          Nets that are applied on array variables will now also create a new recoded +array that reflects the net definitions if recoded is used. The +method has been creating only the item versions before.

                          +
                          +

                          Update: Stack.add_stats()

                          +

                          The method will now create a new metadata property called 'factor' for each +variable it is applied on. You can only have one factor assigned to one +categorical value, so for multiple statistic definitions (exclusions, etc.) +it will get overwritten.

                          +
                          +

                          Update: DataSet.from_batch() (additions parameter)

                          +

                          The additions parameter has been updated to also be able to create recoded +variables from existing “additional” Batches that are attached to a parent one. +Filter variables will get the new meta 'properties' tag 'recoded_filter' +and only have one category (1, 'active'). They are named simply +'filter_1', 'filter_2' and so on. The new possible values of the +parameters are now:

                          +
                          +
                            +
                          • None: as_addition()-Batches are not considered.
                          • +
                          • 'variables': Only cross- and downbreak variables are considered.
                          • +
                          • 'filters': Only filters are recoded.
                          • +
                          • 'full': 'variables' + 'filters'
                          • +
                          +
                          +
                          +

                          Bugfix: ViewManager._request_views()

                          +

                          Cumulative sums are only requested if they are included in the belonging +Stack. Additionally the correct related sig-tests are now taken for +cumulative sums.

                          +
                          +
                          +

                          sd (12/01/2018)

                          +

                          New: Audit

                          +

                          Audit is a new class which takes DataSet instances, compares and aligns +them.

                          +

                          The class compares/ reports/ aligns the following aspects:

                          +
                          +
                            +
                          • datasets are valid (DataSet.validate())
                          • +
                          • mismatches (variables are not included in all datasets)
                          • +
                          • different types (variables are in more than one dataset, but have different types)
                          • +
                          • labels (variables are in more than one dataset, but have different labels for the same text_key)
                          • +
                          • value codes (variables are in more than one dataset, but have different value codes)
                          • +
                          • value texts (variables are in more than one dataset, but have different value texts)
                          • +
                          • array items (arrays are in more than one dataset, but have different items)
                          • +
                          • item labels (arrays are in more than one dataset, but their items have different labels)
                          • +
                          +
                          +

                          This is the first draft of the class, so it will need some testing and probably +adjustments.

                          +
                          +

                          New: DataSet.reorder_items(name, new_order)

                          +

                          The new method reorders the items of the included array. The ints in the +new_order list match up to the number of the items +(DataSet.item_no('item_name')), not to the position.

                          +
                          +

                          New: DataSet.valid_tks, Arabic

                          +

                          Arabic (ar-AR) is included as default valid text-key.

                          +
                          +

                          New: DataSet.extend_items(name, ext_items, text_key=None)

                          +

                          The new method extends the items of an existing array.

                          +
                          +

                          Update: DataSet.set_missings()

                          +

                          The method is now limited to DataSet, Batch does not inherit it.

                          +
                          +

                          Update: DataSet

                          +

                          The whole class is reordered and cleaned up. Some new deprecation warnings +will appear.

                          +
                          +

                          Update: DataSet.add_meta() / DataSet.derive()

                          +

                          Both methods will now raise a ValueError: Duplicated codes provided. Value codes must be unique! +if categorical values definitions try to apply duplicated codes.

                          +
                          +
                          +
                          +

                          sd (18/12/2017)

                          +

                          New: Batch.remove_filter()

                          +

                          Removes all defined (global + extended) filters from a Batch instance.

                          +
                          +

                          Update: Batch.add_filter()

                          +

                          It’s now possible to extend the global filter of a Batch instance. These options +are possible.

                          +

                          Add first filter:

                          +
                          >>> batch.filter, batch.filter_names
                          +'no_filter', ['no_filter']
                          +>>> batch.add_filter('filter1', logic1)
                          +>>> batch.filter, batch.filter_names
                          +{'filter1': logic1}, ['filter1']
                          +
                          +
                          +

                          Extend filter:

                          +
                          >>> batch.filter, batch.filter_names
                          +{'filter1': logic}, ['filter1']
                          +>>> batch.add_filter('filter2', logic2)
                          +>>> batch.filter, batch.filter_names
                          +{'filter1' + 'filter2': intersection([logic1, logic2])}, ['filter1' + 'filter2']
                          +
                          +
                          +

                          Replace filter:

                          +
                          >>> batch.filter, batch.filter_names
                          +{'filter1': logic}, ['filter1']
                          +>>> batch.add_filter('filter1', logic2)
                          +>>> batch.filter, batch.filter_names
                          +{'filter1': logic2}, ['filter1']
                          +
                          +
                          +
                          +

                          Update: Stack.add_stats(..., recode)

                          +

                          The new parameter recode defines if a new numerical variable is created which +satisfies the stat definitions.

                          +
                          +

                          Update: DataSet.populate()

                          +

                          A progress tracker is added to this method.

                          +
                          +

                          Bugfix: Batch.add_open_ends()

                          +

                          = is removed from all responsess in the included variables, as it causes +errors in the Excel-Painter.

                          +
                          +

                          Bugfix: Batch.extend_x() and Batch.extend_y()

                          +

                          Check if included variables exist and unroll included masks.

                          +
                          +

                          Bugfix: Stack.add_nets(..., calc)

                          +

                          If the operator in calc is div/ /, the calculation is now performed +correctly.

                          +
                          +
                          +
                          +

                          sd (28/11/2017)

                          +

                          New DataSet.from_batch()

                          +

                          Creates a new DataSet instance out of Batch definitions (xks, yks, +filter, weight, language, additions, edits).

                          +
                          +

                          New: Batch.add_total()

                          +

                          Defines if total column @ should be included in the downbreaks (yks).

                          +
                          +

                          New: Batch.set_unwgt_counts()

                          +

                          If cellitems are cp and a weight is provided, it is possible to request +unweighted count views (percentages are still weighted).

                          +
                          +

                          Update: Batch.add_y_on_y(name, y_filter=None, main_filter='extend')

                          +

                          Multiple y_on_y aggregations can now be added to a Batch instance +and each can have an own filter. The y_on_y-filter can extend or replace +the main_filter of the Batch.

                          +
                          +

                          Update: Stack.add_nets(..., recode)

                          +

                          The new parameter recode defines if a new variable is created which +satisfies the net definitions. Different options for recode are:

                          +
                          +
                            +
                          • 'extend_codes': The new variable contains all codes of the original +variable and all nets as new categories.
                          • +
                          • 'drop_codes': The new variable contains only all nets as new categories.
                          • +
                          • 'collect_codes' or 'collect_codes@cat_name': The new variable contains +all nets as new categories and another new category which sums all cases that +are not in any net. The new category text can be defined by adding @cat_name +to collect_codes. If none is provided Other is used as default.
                          • +
                          +
                          +
                          +

                          Update: Stack.add_nets()

                          +

                          If a variable in the Stack already has a net_view, it gets overwritten +if a new net is added.

                          +
                          +

                          Update: DataSet.set_missings(..., missing_map)

                          +

                          The parameter missing_map can also handle lists now. All included +codes are be flagged as 'exclude'.

                          +
                          +

                          Update: request_views(..., sums='mid') (ViewManager/query.py)

                          +

                          Allow different positions for sums in the view-order. They can be placed in +the middle ('mid') between the basics/ nets and the stats or at the +'bottom' after the stats.

                          +
                          +

                          Update/ New: write_dimensions()

                          +

                          Converting qp data to mdd and ddf files by using write_dimensions() is +updated now. A bug regarding encoding texts is fixed and additionally all +included text_keys in the meta are transferred into the mdd. Therefore +two new classes are included: DimLabels and DimLabel.

                          +
                          +
                          +

                          sd (13/11/2017)

                          +
                          +
                          New ``DataSet.to_delimited_set(name, label, variables,
                          +
                          from_dichotomous=True, codes_from_name=True)``
                          +
                          +

                          Creates a new delimited set variable out of other variables. If the input- +variables are dichotomous (from_dichotomous), the new value-codes can be +taken from the variable-names or from the order of the variables +(codes_from_name).

                          +
                          +

                          Update Stack.aggregate(..., bases={})

                          +

                          A dictionary in form of:

                          +
                          bases = {
                          +   'cbase': {
                          +      'wgt': True,
                          +      'unwgt': False},
                          +   'cbase_gross': {
                          +      'wgt': True,
                          +      'unwgt': True},
                          +   'ebase': {
                          +      'wgt': False,
                          +      'unwgt': False}
                          +      }
                          +
                          +
                          +

                          defines what kind of bases will be aggregated. If bases is provided the +old parameter unweighted_base and any bases in the parameter views +will be ignored. If bases is not provided and any base is included in views, +a dictionary is automatically created out of views and unweighted_base.

                          +
                          +
                          +

                          sd (17/10/2017)

                          +

                          New: del DataSet['var_name'] and 'var_name' in DataSet syntax support

                          +

                          It is now possible to test membership of a variable name simply using the in +operator instead of DataSet.var_exists('var_name') and delete a variable definition +from DataSet using the del keyword inplace of the drop('var_name') +method.

                          +
                          +

                          New: DataSet.is_single(name), .is_delimited_set(name), .is_int(name), .is_float(name), .is_string(name), .is_date(name), .is_array(name)

                          +

                          These new methods make testing a variable’s type easy.

                          +
                          +

                          Update: DataSet.singles(array_items=True) and all other non-array type iterators

                          +

                          It is now possible to exclude array items from singles(), delimited_sets(), +ints() and floats() variable lists by setting the new array_items +parameter to False.

                          +
                          +

                          Update: Batch.set_sigtests(..., flags=None, test_total=None), Batch.sigproperties

                          +

                          The significancetest-settings for flagging and testing against total, can now +be modified by the two parameters flags and test_total. The Batch +attribute siglevels is removed, instead all sig-settings are stored +in Batch.sigproperties.

                          +
                          +

                          Update: Batch.make_summaries(..., exclusive=False), Batch.skip_items

                          +

                          The new parameter exclusive can take a list of arrays or a boolean. If a list +is included, these arrays are added to Batch.skip_items, if it is True all +variables from Batch.summaries are added to Batch.skip_items

                          +
                          +

                          Update: quantipy.sandbox.sandbox.Chain.paint(..., totalize=True)

                          +

                          If totalize is True, @-Total columns of a (x-oriented) Chain.dataframe +will be painted as 'Total' instead of showing the corresponsing x-variables +question text.

                          +
                          +

                          Update: quantipy.core.weights.Rim.Rake

                          +

                          The weighting algorithm’s generate_report() method can be caught up in a +MemoryError for complex weight schemes run on very large sample sizes. This +is now prevented to ensure the weight factors are computed with priority and +the algorithm is able to terminate correctly. A warning is raised:

                          +
                          UserWarning: OOM: Could not finish writing report...
                          +
                          +
                          +
                          +

                          Update: Batch.replace_y()

                          +

                          Conditional replacements of y-variables of a Batch will now always also +automatically add the @-Total indicator if not provided.

                          +
                          +

                          Bugfix: DataSet.force_texts(...,  overwrite=True)

                          +

                          Forced overwriting of existing text_key meta data was failing for array +mask objects. This is now solved.

                          +
                          +
                          +
                          +

                          sd (15/09/2017)

                          +

                          New: DataSet.meta_to_json(key=None, collection=None)

                          +

                          The new method allows saving parts of the metadata as a json file. The parameters +key and collection define the metaobject which will be saved.

                          +
                          +

                          New: DataSet.save() and DataSet.revert()

                          +

                          These two new methods are useful in interactive sessions like Ipython or +Jupyter notebooks. save() will make a temporary (only im memory, not +written to disk) copy of the DataSet and store its current state. You can +then use revert() to rollback to that snapshot of the data at a later +stage (e.g. a complex recode operation went wrong, reloading from the physical files takes +too long…).

                          +
                          +

                          New: DataSet.by_type(types=None)

                          +

                          The by_type() method is replacing the soon to be deprecated implementation +of variables() (see below). It provides the same functionality +(pd.DataFrame summary of variable types) as the latter.

                          +
                          +

                          Update: DataSet.variables() absorbs list_variables() and variables_from_set()

                          +

                          In conjunction with the addition of by_type(), variables() is +replacing the related list_variables() and variables_from_set() methods in order to offer a unified solution for querying the DataSet’s (main) variable collection.

                          +
                          +

                          Update: Batch.as_addition()

                          +

                          The possibility to add multiple cell item iterations of one Batch definition +via that method has been reintroduced (it was working by accident in previous +versions with subtle side effects and then removed). Have fun!

                          +
                          +

                          Update: Batch.add_open_ends()

                          +

                          The method will now raise an Exception if called on a Batch that has +been added to a parent one via as_addition() to warn the user and prevent +errors at the build stage:

                          +
                          NotImplementedError: Cannot add open end DataFrames to as_addition()-Batches!
                          +
                          +
                          +
                          +
                          +

                          sd (31/08/2017)

                          +

                          New: DataSet.code_from_label(..., exact=True)

                          +

                          The new parameter exact is implemented. If exact=True codes are returned +whose belonging label is equal the included text_label. Otherwise the +method checks if the labels contain the included text_label.

                          +
                          +

                          New: DataSet.order(new_order=None, reposition=None)

                          +

                          This new method can be used to change the global order of the DataSet +variables. You can either pass a complete new_order list of variable names to +set the order or provide a list of dictionaries to move (multiple) variables +before a reference variable name. The order is reflected in the case data +pd.DataFrame.columns order and the meta 'data file' set object’s items.

                          +
                          +

                          New: DataSet.dichotomize(name, value_texts=None, keep_variable_text=True, ignore=None, replace=False, text_key=None)

                          +

                          Use this to convert a 'delimited set' variable into a set of binary coded +'single' variables. Variables will have the values 1/0 and by default use +'Yes' / 'No' as the corresponding labels. Use the value_texts +parameter to apply custom labels.

                          +
                          +

                          New: Batch.extend_x(ext_xks)

                          +

                          The new method enables an easy extension of Batch.xks. In ext_xks +included str are added at the end of Batch.xks. Values of included +dicts are positioned in front of the related key.

                          +
                          +

                          Update: Batch.extend_y(ext_yks, ...)

                          +

                          The parameter ext_yks now also takes dicts, which define the position +of the additional yks.

                          +
                          +

                          Update: Batch.add_open_ends(..., replacements)

                          +

                          The new parameter replacements is implemented. The method loops over the +whole pd.DataFrame and replaces all keys of the included dict +with the belonging value.

                          +
                          +

                          Update: Stack.add_stats(..., other_source)

                          +

                          Statistic views can now be added to delimited sets if other_source is used. +In this case other_source must be a single or numerical variable.

                          +
                          +

                          Update: DataSet.validate(..., spss_limits=False)

                          +

                          The new parameter spss_limits is implemented. If spss_limits=True, the +validate output dataframe is extended by 3 columns which show if the SPSS label +limitations are satisfied.

                          +
                          +

                          Bugfix: DataSet.convert()

                          +

                          A bug that prevented conversions from single to numeric types has been fixed.

                          +
                          +

                          Bugfix: DataSet.add_meta()

                          +

                          A bug that prevented the creation of numerical arrays outside of to.array() +has been fixed. It is now possible to create array metadata without providing +category references.

                          +
                          +

                          Bugfix: Stack.add_stats()

                          +

                          Checking the statistic views is skipped now if no single typed variables are +included even if a checking cluster is provided.

                          +
                          +

                          Bugfix: Batch.copy()

                          +

                          Instead of using a deepcopy of the Batch instance, a new instance is created +and filled with the attributes of the initial one. Then the copied instance can +be used as additional Batch.

                          +
                          +

                          Bugfix: qp.core.builds.powerpoint

                          +

                          Access to bar-chart series and colour-filling is now working for +different Powerpoint versions. Also a bug is fixed which came up in +PowerPointpainter() for variables which have fixed categories and whose +values are located in lib.

                          +
                          +
                          +

                          sd (24/07/2017)

                          +

                          New: qp.set_option()

                          +

                          It is now possible to set library-wide settings registered in qp.OPTIONS +by providing the setting’s name (key) and the desired value. Currently supported +are:

                          +
                          OPTIONS = {
                          +        'new_rules': False,
                          +        'new_chains': False,
                          +        'short_item_texts': False
                          +}
                          +
                          +
                          +

                          So for example, to work with the currently refactored Chain interim class +we can use qp.set_options('new_chains', True).

                          +
                          +

                          New: qp.Batch()

                          +

                          This is a new object aimed at defining and structuring aggregation and build +setups. Please see an extensive overview here.

                          +
                          +

                          New: Stack.aggregate() / add_nets() / add_stats() / add_tests() / …

                          +

                          Connected to the new Batch class, some new Stack methods to ease up +view creation have been added. You can find the docs here.

                          +
                          +

                          New: DataSet.populate()

                          +

                          Use this to create a qp.Stack from Batch definitions. This connects the +Batch and Stack objects; check out the Batch +and Analysis & aggregation docs.

                          +
                          +

                          New: DataSet.write_dimensions(path_mdd=None, path_ddf=None, text_key=None, mdm_lang='ENG', run=True, clean_up=True)

                          +

                          It is now possible to directly convert a DataSet into a Dimensions .ddf/.mdd +file pair (given SPSS Data Collection Base Professional is installed on your +machine). By default, files will be saved to the same location in that the +DataSet resides and keep its text_key.

                          +
                          +

                          New: DataSet.repair()

                          +

                          This new method can be used to try to fix common DataSet metadata problems +stemming from outdated versions, incorrect manual editing of the meta dictionary +or other inconsistencies. The method is checking and repairing following issues:

                          +
                          +
                            +
                          • 'name' is present for all variable metadata
                          • +
                          • 'source' and 'subtype' references for array variables
                          • +
                          • correct 'lib'-based 'values' object for array variables
                          • +
                          • text key-dependent 'x edits' / 'y edits' meta data
                          • +
                          • ['data file']['items'] set entries exist in 'columns' / 'masks'
                          • +
                          +
                          +
                          +

                          New: DataSet.subset(variables=None, from_set=None, inplace=False)

                          +

                          As a counterpart to filter(), subset() can be used to create a new +DataSet that contains only a selection of variables. The new variables +collection can be provided either as a list of names or by naming an already +existing set containing the desired variables.

                          +
                          +

                          New: DataSet.variables_from_set(setname)

                          +

                          Get the list of variables belonging to the passed set indicated by +setname.

                          +
                          +

                          New: DataSet.is_like_numeric(name)

                          +

                          A new method to test if all of a string variable’s values can be converted +to a numerical (int / float) type. Returns a boolean True / False.

                          +
                          +

                          Update: DataSet.convert()

                          +

                          It is now possible to convert inplace from string to int / float if +the respective internal is_like_numeric() check identifies numeric-like values.

                          +
                          +

                          Update: DataSet.from_components(..., reset=True), DataSet.read_quantipy(..., reset=True)

                          +

                          Loaded .json metadata dictionaries will get cleaned now by default from any +user-defined, non-native objects inside the 'lib' and 'sets' +collections. Set reset=False to keep any extra entires (restoring the old +behaviour).

                          +
                          +

                          Update: DataSet.from_components(data_df, meta_dict=None, ...)

                          +

                          It is now possible to create a DataSet instance by providing a pd.DataFrame +alone, without any accompanying meta data. While reading in the case data, the meta +component will be created by inferring the proper Quantipy variable types +from the pandas dtype information.

                          +
                          +

                          Update: Quantity.swap(var, ..., update_axis_def=True)

                          +

                          It is now possible to swap() the 'x' variable of an array based Quantity, +as long as the length oh the constructing 'items' collection is identical. +In addition, the new parameter update_axis_def is now by default enforcing +an update of the axis defintions (pd.DataFrame column names, etc) while +previously the method was keeping the original index and column names. The old +behaviour can be restored by setting the parameter to False.

                          +

                          Array example:

                          +
                          >>> link = stack[name_data]['no_filter']['q5']['@']
                          +>>> q = qp.Quantity(link)
                          +>>> q.summarize()
                          +Array                     q5
                          +Questions               q5_1         q5_2         q5_3         q5_4         q5_5         q5_6
                          +Question Values
                          +q5       All     8255.000000  8255.000000  8255.000000  8255.000000  8255.000000  8255.000000
                          +         mean      26.410297    22.260569    25.181466    39.842883    24.399758    28.972017
                          +         stddev    40.415559    38.060583    40.018463    46.012205    40.537497    41.903322
                          +         min        1.000000     1.000000     1.000000     1.000000     1.000000     1.000000
                          +         25%        3.000000     3.000000     3.000000     3.000000     1.000000     3.000000
                          +         median     5.000000     3.000000     3.000000     5.000000     3.000000     5.000000
                          +         75%        5.000000     5.000000     5.000000    98.000000     5.000000    97.000000
                          +         max       98.000000    98.000000    98.000000    98.000000    98.000000    98.000000
                          +
                          +
                          +

                          Updated axis definiton:

                          +
                          >>> q.swap('q7', update_axis_def=True)
                          +>>> q.summarize()
                          +Array                     q7
                          +Questions               q7_1         q7_2         q7_3       q7_4       q7_5       q7_6
                          +Question Values
                          +q7       All     1195.000000  1413.000000  3378.000000  35.000000  43.000000  36.000000
                          +         mean       5.782427     5.423213     5.795145   4.228571   4.558140   5.333333
                          +         stddev     2.277894     2.157226     2.366247   2.073442   2.322789   2.552310
                          +         min        1.000000     1.000000     1.000000   1.000000   1.000000   1.000000
                          +         25%        4.000000     4.000000     4.000000   3.000000   3.000000   3.000000
                          +         median     6.000000     6.000000     6.000000   4.000000   4.000000   6.000000
                          +         75%        8.000000     7.000000     8.000000   6.000000   6.000000   7.750000
                          +         max        9.000000     9.000000     9.000000   8.000000   9.000000   9.000000
                          +
                          +
                          +

                          Original axis definiton:

                          +
                          >>> q = qp.Quantity(link)
                          +>>> q.swap('q7', update_axis_def=False)
                          +>>> q.summarize()
                          +Array                     q5
                          +Questions               q5_1         q5_2         q5_3       q5_4       q5_5       q5_6
                          +Question Values
                          +q5       All     1195.000000  1413.000000  3378.000000  35.000000  43.000000  36.000000
                          +         mean       5.782427     5.423213     5.795145   4.228571   4.558140   5.333333
                          +         stddev     2.277894     2.157226     2.366247   2.073442   2.322789   2.552310
                          +         min        1.000000     1.000000     1.000000   1.000000   1.000000   1.000000
                          +         25%        4.000000     4.000000     4.000000   3.000000   3.000000   3.000000
                          +         median     6.000000     6.000000     6.000000   4.000000   4.000000   6.000000
                          +         75%        8.000000     7.000000     8.000000   6.000000   6.000000   7.750000
                          +         max        9.000000     9.000000     9.000000   8.000000   9.000000   9.000000
                          +
                          +
                          +
                          +

                          Update: DataSet.merge_texts()

                          +

                          The method will now always overwrite existing text_key meta, which makes it +possible to merge texts from meta of the same text_key as the master +DataSet.

                          +
                          +

                          Bugfix: DataSet.band()

                          +

                          band(new_name=None)’s automatic name generation was incorrectly creating +new variables with the name None_banded. This is now fixed.

                          +
                          +

                          Bugfix: DataSet.copy()

                          +

                          The method will now check if the name of the copy already exists in the +DataSet and drop the referenced variable if found to prevent +inconsistencies. Additionally, it is not longer possible to copy isolated +array items:

                          +
                          >>> dataset.copy('q5_1')
                          +NotImplementedError: Cannot make isolated copy of array item 'q5_1'. Please copy array variable 'q5' instead!
                          +
                          +
                          +
                          +
                          +

                          sd (08/06/2017)

                          +

                          New: DataSet.extend_valid_tks(), DataSet.valid_tks

                          +

                          DataSet has a new attribute valid_tks that contains a list of all valid +textkeys. All methods that take a textkey as parameter are checked against that +list.

                          +

                          If a datafile contains a special/ unusual textkey (for example 'id-ID' or +'zh-TW'), the list can be extended with DataSet.extend_valid_tks(). +This extension can also be used to create a textkey for special conditions, +for example to create texts only for powerpoint outputs:

                          +
                          >>> dataset.extend_valid_tks('pptx')
                          +>>> dataset.force_texts('pptx', 'en-GB')
                          +>>> dataset.set_variable_text('gender','Gender label for pptx', text_key='pptx')
                          +
                          +
                          +
                          +

                          New: Equal error messages

                          +

                          All methods that use the parameters name/var, text_key or +axis_edit/ axis now have a decorator that checks the provided values. +The following shows a few examples for the new error messages:

                          +

                          name & var:

                          +
                          'name' argument for meta() must be in ['columns', 'masks'].
                          +q1 is not in ['columns', 'masks'].
                          +
                          +
                          +

                          text_key:

                          +
                          'en-gb' is not a valid text_key! Supported are: ['en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE']
                          +
                          +
                          +

                          axis_edit & axis:

                          +
                          'xs' is not a valid axis! Supported are: ['x', 'y']
                          +
                          +
                          +
                          +

                          New: DataSet.repair_text_edits(text_key)

                          +

                          This new method can be used in trackers, that were drawn up in an older Quantipy +version. Text objects can be repaired if are not well prepared, for example if +it looks like this:

                          +
                          {'en-GB': 'some English text',
                          + 'sv_SE': 'some Swedish text',
                          + 'x edits': 'new text'}
                          +
                          +
                          +

                          DataSet.repair_text_edits() loops over all text objects in the dataset and +matches the x edits and y edits texts to all included textkeys:

                          +
                          >>> dataset.repair_text_edits(['en-GB', 'sv-SE'])
                          +{'en-GB': 'some English text',
                          + 'sv_SE': 'some Swedish text',
                          + 'x edits': {'en-GB': new text', 'sv-SE': 'new text'}}
                          +
                          +
                          +
                          +

                          Update: DataSet.meta()/ .text()/ .values()/ .value_texts()/ .items()/ .item_texts()

                          +

                          All these methods now can take the parameters text_key and axis_edit. +The related text is taken from the meta information and shown in the output. +If a text key or axis edit is not included the text is returned as None.

                          +
                          +

                          Update: DataSet.compare(dataset, variables=None, strict=False, text_key=None)

                          +

                          The method is totally updated, works more precise and contains a few new +features. Generally variables included in dataset are compared with +eponymous variables in the main DataSet instance. You can specify witch +variables should be compared, if question/ value texts should be compared +strict or not and for which text_key.

                          +
                          +

                          Update: DataSet.validate(verbose=True)

                          +

                          A few new features are tested now and the output has changed. Set verbose=True +to see the definitions of the different error columns:

                          +
                          name: column/mask name and meta[collection][var]['name'] are not identical
                          +
                          +q_label: text object is badly formated or has empty text mapping
                          +
                          +values: categorical var does not contain values, value text is badly
                          +formated or has empty text mapping
                          +
                          +textkeys: dataset.text_key is not included or existing tks are not
                          +consistent (also for parents)
                          +
                          +source: parents or items do not exist
                          +
                          +codes: codes in .data are not included in .meta
                          +
                          +
                          +
                          +

                          Update: DataSet.sorting() / .slicing() / .hiding()

                          +

                          These methods will now also work on lists of variable names.

                          +
                          +

                          Update: DataSet.set_variable_text(), Dataset.set_item_texts()

                          +

                          If these methods are applied to an array item, the new variable text is also +included in the meta information of the parent array. The same works also the +other way around, if an array text is set, then the array item texts are modified.

                          +
                          +

                          Update: DataSet.__init__(self, name, dimensions_comp=True)

                          +

                          A few new features are included to handle data coming from Crunch. While +initializing a new DataSet instance dimensions compatibility can be set to +False. In the custom template use t.get_qp_dataset(name, dim_comp=False) +in the load cells.

                          +
                          +

                          Bugfix: DataSet.hmerge()

                          +

                          If right_on and left_on are used and right_on is also included in +the main file, it is not overwritten any more.

                          +
                          +
                          +

                          sd (17/05/2017)

                          +

                          Update: DataSet.set_variable_text(..., axis_edit=None), DataSet.set_value_texts(..., axis_edit=False)

                          +

                          The new axis_edit argument can be used with one of 'x', 'y' or ['x', 'y'] to instruct a text metadata change that will only be visible in build exports.

                          +
                          +

                          Warning

                          +

                          In a future version set_col_text_edit() and set_val_text_text() will +be removed! The identical functionality is provided via this axis_edit parameter.

                          +
                          +
                          +

                          Update: DataSet.replace_texts(..., text_key=None)

                          +

                          The method loops over all meta text objects and replaces unwanted strings. +It is now possible to perform the replacement only for specified text_keys. +If text_key=None the method replaces the strings for all text_keys.

                          +
                          +

                          Update: DataSet.force_texts(copy_to=None, copy_from=None, update_existing=False)

                          +

                          The method is now only able to force texts for all meta text objects (for +single variables use the methods set_variable_text() and +set_value_texts()).

                          +
                          +

                          Bugfix: DataSet.copy()

                          +

                          Copied variables get the tag created and can be listed with +t.list_variables(dataset, 'created').

                          +
                          +

                          Bugfix: DataSet.hmerge(), DataSet.vmerge()

                          +

                          Array meta information in merged datafiles is now updated correctly.

                          +
                          +
                          +

                          sd (04/05/2017)

                          +

                          New: DataSet.var_exists()

                          +

                          Returns True if the input variable/ list of variables are included in the +DataSet instance, otherwise False.

                          +
                          +

                          New: DataSet.remove_html(), DataSet.replace_texts(replace)

                          +

                          The DataSet method clean_texts() has been removed and split into two +methods to make usage more clear: remove_html() will strip all text +metadata objects from any html and formatting tags. replace_texts() will +use a dict mapping of old to new str terms to change the matching +text throughout the DataSet metadata.

                          +
                          +

                          New: DataSet.item_no(name)

                          +

                          This method will return the positional index number of an array item, e.g.:

                          +
                          >>> dataset.item_no('Q4A[{q4a_1}].Q4A_grid')
                          +1
                          +
                          +
                          +
                          +

                          New: QuantipyViews: counts_cumsum, c%_cumsum

                          +

                          These two new views contain frequencies with cumulative sums which are computed +over the x-axis.

                          +
                          +

                          Update: DataSet.text(name, shorten=True)

                          +

                          The new parameter shorten is now controlling if the variable text metadata +of array masks will be reported in short format, i.e. without the corresponding +mask label text. This is now also the default behaviour.

                          +
                          +

                          Update: DataSet.to_array()

                          +

                          Created mask meta information now also contains keys parent and subtype. +Variable names are compatible with crunch and dimensions meta:

                          +

                          Example in Dimensions modus:

                          +
                          >>> dataset.to_array('Q11', ['Q1', 'Q2', 'Q3', 'Q4', 'Q5'], 'label')
                          +
                          +
                          +

                          The new grid is named 'Q11.Q11_grid' and the source/column variables are +'Q11[{Q1}].-Q11_grid' - 'Q11[{Q5}].-Q11_grid'.

                          +
                          +

                          Bugfix: DataSet.derotate()

                          +

                          Meta is now Crunch and Dimensions compatible. Also mask meta information are updated.

                          +
                          +
                          +

                          sd (24/04/2017)

                          +

                          Update: DataSet.hiding(..., hide_values=True)

                          +

                          The new parameter hide_values is only necessary if the input variable is a +mask. If False, mask items are hidden, if True mask values are hidden +for all mask items and for array summary sheets.

                          +
                          +

                          Bugfix: DataSet.set_col_text_edit(name)

                          +

                          If the input variable is an array item, the new column text is also added to +meta['mask'][name]['items].

                          +
                          +

                          Bugfix: DataSet.drop(name, ignore_items=False)

                          +

                          If a mask is dropped, but the items are kept, all items are handled now as +individual variables and their meta information is not stored in meta['lib'] +anymore.

                          +
                          +
                          +

                          sd (06/04/2017)

                          +

                          Only small adjustments.

                          +
                          +
                          +

                          sd (29/03/2017)

                          +

                          New: DataSet.codes_in_data(name)

                          +

                          This method returns a list of codes that exist in the data of a variable. This +information can be used for more complex recodes, for example copying a variable, +but keeping only all categories with more than 50 ratings, e.g.:

                          +
                          >>> valid_code = dataset.codes_in_data('varname')
                          +>>> keep_code = [x for x in valid_code if dataset['varname'].value_counts()[x] > 49]
                          +>>> dataset.copy('varname', 'rc', copy_only=keep_code)
                          +
                          +
                          +
                          +

                          Update: DataSet.copy(..., copy_not=None)

                          +

                          The new parameter copy_not takes a list of codes that should be ignored +for the copied version of the provided variable. The metadata of the copy will +be reduced as well.

                          +
                          +

                          Update: DataSet.code_count()

                          +

                          This method is now alligned with any() and all() in that it can be used +on 'array' variables as well. In such a case, the resulting pandas.Series +is reporting the number of answer codes found across all items per case data +row, i.e.:

                          +
                          >>> code_count = dataset.code_count('Q4A.Q4A_grid', count_only=[3, 4])
                          +>>> check = pd.concat([dataset['Q4A.Q4A_grid'], code_count], axis=1)
                          +>>> check.head(10)
                          +   Q4A[{q4a_1}].Q4A_grid  Q4A[{q4a_2}].Q4A_grid  Q4A[{q4a_3}].Q4A_grid  0
                          +0                    3.0                    3.0                    NaN  2
                          +1                    NaN                    NaN                    NaN  0
                          +2                    3.0                    3.0                    4.0  3
                          +3                    5.0                    4.0                    2.0  1
                          +4                    4.0                    4.0                    4.0  3
                          +5                    4.0                    5.0                    4.0  2
                          +6                    3.0                    3.0                    3.0  3
                          +7                    4.0                    4.0                    4.0  3
                          +8                    6.0                    6.0                    6.0  0
                          +9                    4.0                    5.0                    5.0  1
                          +
                          +
                          +
                          +
                          +
                          +

                          sd (20/03/2017)

                          +

                          New: qp.DataSet(dimensions_comp=True)

                          +

                          The DataSet class can now be explicitly run in a Dimensions compatibility +mode to control the naming conventions of array variables (“grids”). This +is also the default behaviour for now. This comes with a few changes related to +meta creation and variable access using DataSet methods. Please see a brief +case study on this topic here.

                          +
                          +

                          New: enriched items / masks meta data

                          +

                          masks will now also store the subtype (single, delimited set, etc.) +while items elements will now contain a reference to the defining masks +entrie(s) in a new parent object.

                          +
                          +

                          Update: DataSet.weight(..., subset=None)

                          +

                          Filters the dataset by giving a Quantipy complex logic expression and weights +only the remaining subset.

                          +
                          +

                          Update: Defining categorical values meta and array items

                          +

                          Both values and items can now be created in three different ways when +working with the DataSet methods add_meta(), extend_values() and +derive(): (1) Tuples that map element code to label, (2) only labels or (3) +only element codes. Please see quick guide on that here

                          +
                          +
                          +

                          sd (07/03/2017)

                          +

                          Update: DataSet.code_count(..., count_not=None)

                          +

                          The new parameter count_not can be used to restrict the set of codes feeding +into the resulting pd.Series by exclusion (while count_only restricts +by inclusion).

                          +
                          +

                          Update: DataSet.copy(..., copy_only=None)

                          +

                          The new parameter copy_only takes a list of codes that should be included +for the copied version of the provided variable, all others will be ignored +and the metadata of the copy will be reduced as well.

                          +
                          +

                          Bugfix: DataSet.band()

                          +

                          There was a bug that was causing the method to crash for negative values. It is +now possible to create negative single value bands, while negative ranges +(lower and/or upper bound < 0) will raise a ValueError.

                          +
                          +
                          +
                          +

                          sd (24/02/2017)

                          +
                            +
                          • Some minor bugfixes and updates. Please use latest version.
                          • +
                          +
                          +
                          +
                          +

                          sd (16/02/2017)

                          +

                          New: DataSet.derotate(levels, mapper, other=None, unique_key='identity', dropna=True)

                          +

                          Create a derotated (“levelled”, responses-to-cases) DataSet instance by +defining level variables, looped variables and other (simple) variables that +should be added.

                          +

                          View more information on the topic here.

                          +
                          +

                          New: DataSet.to_array(name, variables, label)

                          +

                          Combine column variables with identical values objects to an array +incl. all required meta['masks'] information.

                          +
                          +

                          Update: DataSet.interlock(..., variables)

                          +

                          It is now possible to add dicts to variables. In these dicts a +derive()-like mapper can be included which will then create a temporary +variable for the interlocked result. Example:

                          +
                          >>> variables = ['gender',
                          +...              {'agegrp': [(1, '18-34', {'age': frange('18-34')}),
                          +...                          (2, '35-54', {'age': frange('35-54')}),
                          +...                          (3, '55+', {'age': is_ge(55)})]},
                          +...              'region']
                          +>>> dataset.interlock('new_var', 'label', variables)
                          +
                          +
                          +
                          +
                          +
                          +

                          sd (04/01/2017)

                          +

                          New: DataSet.flatten(name, codes, new_name=None, text_key=None)

                          +

                          Creates a new delimited set variable that groups grid item answers to +categories. The items become values of the new variable. If an +item contains one of the codes it will be counted towards the categorical +case data of the new variable.

                          +
                          +

                          New: DataSet.uncode(target, mapper, default=None, intersect=None, inplace=True)

                          +

                          Remove codes from the target variable’s data component if a logical +condition is satisfied.

                          +
                          +

                          New: DataSet.text(var, text_key=None)

                          +

                          Returns the question text label (per text_key) of a variable.

                          +
                          +

                          New: DataSet.unroll(varlist, keep=None, both=None)

                          +

                          Replaces masks names inside varlist with their items. Optionally, +individual masks can be excluded or kept inside the list.

                          +
                          +

                          New: DataSet.from_stack(stack, datakey=None)

                          +

                          Create a quantipy.DataSet from the meta, data, data_key and +filter definition of a quantipy.Stack instance.

                          +
                          +
                          +
                          +

                          sd (8/12/2016)

                          +

                          New:

                          +

                          DataSet.from_excel(path_xlsx, merge=True, unique_key='identity')

                          +

                          Returns a new DataSet instance with data from excel. The meta +for all variables contains type='int'.

                          +

                          Example: new_ds = dataset.from_excel(path, True, 'identity')

                          +

                          The function is able to modify dataset inplace by merging new_ds on +identity.

                          +
                          +

                          Update:

                          +

                          DataSet.copy(..., slicer=None)

                          +

                          It is now possible to filter the data that statisfies the logical condition +provided in the slicer. +Example:

                          +
                          >>> dataset.copy('q1', 'rec', True, {'q1': not_any([99])})
                          +
                          +
                          +
                          +
                          +
                          +

                          sd (23/11/2016)

                          +

                          Update:

                          +

                          DataSet.rename(name, new_name=None, array_item=None)

                          +

                          The function is able to rename columns, masks or mask items. +maks items are changed by position.

                          +
                          +

                          Update:

                          +

                          DataSet.categorize(..., categorized_name=None)

                          +

                          Provide a custom name string for categorized_name will change the default +name of the categorized variable from OLD_NAME# to the passed string.

                          +
                          +
                          +
                          +

                          sd (16/11/2016)

                          +

                          New:

                          +

                          DataSet.check_dupe(name='identity')

                          +

                          Returns a list with duplicated values for the variable provided via name. +Identifies for example duplicated identities.

                          +
                          +

                          New:

                          +

                          DataSet.start_meta(text_key=None)

                          +

                          Creates an empty QP meta data document blueprint to add variable definitions to.

                          +
                          +

                          Update:

                          +
                          DataSet.create_set(setname='new_set', based_on='data file', included=None,
                          +...               excluded=None, strings='keep', arrays='both', replace=None,
                          +...               overwrite=False)
                          +
                          +
                          +

                          Add a new set to the meta['sets'] object. Variables from an existing +set (based_on) can be included to new_set or varibles can be +excluded from based_on with customized lists of variables. +Control string variables and masks with the kwargs strings and +arrays. replace single variables in new_set with a dict .

                          +
                          +

                          Update:

                          +

                          DataSet.from_components(..., text_key=None)

                          +

                          Will now accept a text_key in the method call. If querying a text_key +from the meta component fails, the method will no longer crash, but raise a +warning and set the text_key to None.

                          +
                          +

                          Update:

                          +
                          +
                          DataSet.as_float()
                          +
                          DataSet.as_int()
                          +
                          DataSet.as_single()
                          +
                          DataSet.as_delimited_set()
                          +
                          DataSet.as_string()
                          +
                          DataSet.band_numerical()
                          +
                          DataSet.derive_categorical()
                          +
                          DataSet.set_mask_text()
                          +
                          DataSet.set_column_text()
                          +
                          +

                          These methods will now print a UserWarning to prepare for the soon to +come removal of them.

                          +
                          +

                          Bugfix:

                          +

                          DataSet.__setitem__()

                          +

                          Trying to set np.NaN was failing the test against meta data for categorical +variables and was raising a ValueError then. This is fixed now.

                          +
                          +
                          +
                          +

                          sd (11/11/2016)

                          +

                          New:

                          +
                          +
                          DataSet.columns
                          +
                          DataSet.masks
                          +
                          DataSet.sets
                          +
                          DataSet.singles
                          +
                          DataSet.delimited_sets
                          +
                          DataSet.ints
                          +
                          DataSet.floats
                          +
                          DataSet.dates
                          +
                          DataSet.strings
                          +
                          +

                          New DataSet instance attributes to quickly return the list of columns, +masks and sets objects from the meta or query the variables by +type. Use this to check for variables, iteration, inspection, ect.

                          +
                          +

                          New:

                          +

                          DataSet.categorize(name)

                          +

                          Create a categorized version of int/string/date variables. New variables +will be named as per OLD_NAME#

                          +
                          +

                          New:

                          +

                          DataSet.convert(name, to)

                          +

                          Wraps the individual as_TYPE() conversion methods. to must be one of +'int', 'float', 'string', 'single', 'delimited set'.

                          +
                          +

                          New:

                          +

                          DataSet.as_string(name)

                          +

                          Only for completeness: Use DataSet.convert(name, to='string') instead.

                          +

                          Converts int/float/single/date typed variables into a string and +removes all categorical metadata.

                          +
                          +

                          Update:

                          +

                          DataSet.add_meta()

                          +

                          Can now add date and text type meta data.

                          +
                          +

                          Bugfix:

                          +

                          DataSet.vmerge()

                          +

                          If masks in the right dataset, that also exist in the left dataset, +have new items or values, they are added to meta['masks'], +meta['lib'] and meta['sets'].

                          +
                          +
                          +
                          +

                          sd (09/11/2016)

                          +

                          New:

                          +

                          DataSet.as_float(name)

                          +

                          Converts int/single typed variables into a float and removes +all categorical metadata.

                          +
                          +

                          New:

                          +

                          DataSet.as_int(name)

                          +

                          Converts single typed variables into a int and removes +all categorical metadata.

                          +
                          +

                          New:

                          +

                          DataSet.as_single(name)

                          +

                          Converts int typed variables into a single and adds numeric values as +categorical metadata.

                          +
                          +

                          New:

                          +

                          DataSet.create_set(name, variables, blacklist=None)

                          +

                          Adds a new set to meta['sets'] object. Create easily sets from +other sets while using customised blacklist.

                          +
                          +

                          New:

                          +

                          DataSet.drop(name, ignore_items=False)

                          +

                          Removes all metadata and data referenced to the variable. When passing an +array mask, ignore_items can be ste to True to keep the item +columns incl. their metadata.

                          +
                          +

                          New:

                          +

                          DataSet.compare(dataset=None, variables=None)

                          +

                          Compare the metadata definition between the current and another dataset, +optionally restricting to a pair of variables.

                          +
                          +

                          Update:

                          +

                          DataSet.__setitem__()

                          +

                          [..]-Indexer now checks scalars against categorical meta.

                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/03_how_to_snippets.html b/docs/API/_build/html/sites/release_notes/03_how_to_snippets.html new file mode 100644 index 000000000..d8eeee405 --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/03_how_to_snippets.html @@ -0,0 +1,444 @@ + + + + + + + + + + + How-to-snippets — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          + + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/how_to_snippets/create_categorical_meta.html b/docs/API/_build/html/sites/release_notes/how_to_snippets/create_categorical_meta.html new file mode 100644 index 000000000..e9c422c5e --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/how_to_snippets/create_categorical_meta.html @@ -0,0 +1,492 @@ + + + + + + + + + + + Different ways of creating categorical values — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Different ways of creating categorical values

                          +

                          The DataSet methods add_meta(), extend_values() and derive() +offer three alternatives for specifying the categorical values of 'single' +and 'delimited set' typed variables. The approaches differ with respect to +how the mapping of numerical value codes to value text labels is handled.

                          +

                          (1) Providing a list of text labels

                          +

                          By providing the category labels only as a list of str, DataSet +is going to create the numerical codes by simple enumeration:

                          +
                          >>> name, qtype, label = 'test_var', 'single', 'The test variable label'
                          +
                          +
                          +
                          >>> cats = ['test_cat_1', 'test_cat_2', 'test_cat_3']
                          +>>> dataset.add_meta(name, qtype, label, cats)
                          +
                          +
                          +
                          >>> dataset.meta('test_var')
                          +single                             codes       texts missing
                          +test_var: The test variable label
                          +1                                      1  test_cat_1    None
                          +2                                      2  test_cat_2    None
                          +3                                      3  test_cat_3    None
                          +
                          +
                          +

                          (2) Providing a list of numerical codes

                          +

                          If only the desired numerical codes are provided, the label information for all +categories consequently will appear blank. In such a case the user will, however, +get reminded to add the 'text' meta in a separate step:

                          +
                          >>> cats = [1, 2, 98]
                          +>>> dataset.add_meta(name, qtype, label, cats)
                          +...\\quantipy\core\dataset.py:1287: UserWarning: 'text' label information missing,
                          +only numerical codes created for the values object. Remember to add value 'text' metadata manually!
                          +
                          +
                          +
                          >>> dataset.meta('test_var')
                          +single                             codes texts missing
                          +test_var: The test variable label
                          +1                                      1          None
                          +2                                      2          None
                          +3                                     98          None
                          +
                          +
                          +

                          (3) Pairing numerical codes with text labels

                          +

                          To explicitly assign codes to corresponding labels, categories can also be +defined as a list of tuples of codes and labels:

                          +
                          >>> cats = [(1, 'test_cat_1') (2, 'test_cat_2'), (98, 'Don\'t know')]
                          +>>> dataset.add_meta(name, qtype, label, cats)
                          +
                          +
                          +
                          >>> dataset.meta('test_var')
                          +single                             codes       texts missing
                          +test_var: The test variable label
                          +1                                      1  test_cat_1    None
                          +2                                      2  test_cat_2    None
                          +3                                     98  Don't know    None
                          +
                          +
                          +
                          +

                          Note

                          +

                          All three approaches are also valid for defining the items object for +array-typed masks.

                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/how_to_snippets/derotate.html b/docs/API/_build/html/sites/release_notes/how_to_snippets/derotate.html new file mode 100644 index 000000000..b6f86d4a6 --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/how_to_snippets/derotate.html @@ -0,0 +1,677 @@ + + + + + + + + + + + Derotation — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Derotation

                          +
                          +

                          What is derotation

                          +

                          Derotation of data is necessary if brands, products or something similar +(levels) are assessed and each respondent (case) rates a different +selection of that levels. So each case has several responses. +Derotation now means, that the data is switched from case-level to +responses-level.

                          +

                          Example: q1_1/q1_2: On a scale from 1 to 10, how much do you like the +following drinks?

                          +
                          +
                          1: water
                          +
                          2: cola
                          +
                          3: lemonade
                          +
                          4: beer
                          +

                          +
                          +

                          ``data``

                          + ++++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          iddrink_1drink_2q1_1q1_2gender
                          case113281
                          case214952
                          case3246101
                          +

                          derotated ``data``

                          + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                           drinkdrink_levelledq1gender
                          case11121
                          case12381
                          case21192
                          case22452
                          case31261
                          case324101
                          +

                          To identify which case rates which levels, some key-/level-variables are +included in the data, in this example drink_1 and drink_2. +Variables (for example gender) that are not included to this loop can also +be added.

                          +
                          +
                          +

                          How to use DataSet.derotate()

                          +

                          The DataSet method takes a few parameters:

                          +
                            +
                          • levels: dict of list

                            +

                            Contains all key-/level-variables and the name for the new levelled variable. +All key-/level-variables must have the same value_map.

                            +
                            >>> levels = {'drink': ['drink_1', 'drink_2']}
                            +
                            +
                            +
                          • +
                          +
                          +

                          +
                          +
                            +
                          • mapper: list of dicts of list

                            +

                            Contains the looped questions and the new column name to which the +looped questions will be combinded.

                            +
                            >>> mapper = [{'q1': ['q1_1', 'q1_2']}]
                            +
                            +
                            +
                          • +
                          +
                          +

                          +
                          +
                            +
                          • other: str or list of str

                            +

                            Contains all variables that should be assumed to the derotated data, but +which are not included in the loop.

                            +
                            >>> other = 'gender'
                            +
                            +
                            +
                          • +
                          +
                          +

                          +
                          +
                            +
                          • unique_key: str

                            +

                            Name of varibale that identifies cases in the initial data.

                            +
                            >>> unique_key = 'id'
                            +
                            +
                            +
                          • +
                          +
                          +

                          +
                          +
                            +
                          • dropna: bool, default True

                            +

                            If a case rates less then the possible counts of levels, these responses +will be droped.

                            +
                          • +
                          +
                          >>> ds = dataset.derotate(levels = {'drink': ['drink_1', 'drink_2']},
                          +...                       mapper = [{'q1': ['q1_1', 'q1_2']}],
                          +...                       other = 'gender',
                          +...                       unique_key = 'id',
                          +...                       dropna = True)
                          +
                          +
                          +
                          +
                          +

                          What about arrays?

                          +

                          It is possible that also arrays are looped. In this case a mapper can look +like this:

                          +
                          >>> mapper = [{'q12_1': ['q12a[{q12a_1}].q12a_grid', 'q12b[{q12b_1}].q12b_grid',
                          +...                      'q12c[{q12c_1}].q12c_grid', 'q12d[{q12d_1}].q12d_grid']},
                          +...           {'q12_2': ['q12a[{q12a_2}].q12a_grid', 'q12b[{q12b_2}].q12b_grid',
                          +...                      'q12c[{q12c_2}].q12c_grid', 'q12d[{q12d_2}].q12d_grid']},
                          +...           {'q12_3': ['q12a[{q12a_3}].q12a_grid', 'q12b[{q12b_3}].q12b_grid',
                          +...                      'q12c[{q12c_3}].q12c_grid', 'q12d[{q12d_3}].q12d_grid']},
                          +...           {'q12_4': ['q12a[{q12a_4}].q12a_grid', 'q12b[{q12b_4}].q12b_grid',
                          +...                      'q12c[{q12c_4}].q12c_grid', 'q12d[{q12d_4}].q12d_grid']},
                          +...           {'q12_5': ['q12a[{q12a_5}].q12a_grid', 'q12b[{q12b_5}].q12b_grid',
                          +...                      'q12c[{q12c_5}].q12c_grid', 'q12d[{q12d_5}].q12d_grid']},
                          +...           {'q12_6': ['q12a[{q12a_6}].q12a_grid', 'q12b[{q12b_6}].q12b_grid',
                          +...                      'q12c[{q12c_6}].q12c_grid', 'q12d[{q12d_6}].q12d_grid']},
                          +...           {'q12_7': ['q12a[{q12a_7}].q12a_grid', 'q12b[{q12b_7}].q12b_grid',
                          +...                      'q12c[{q12c_7}].q12c_grid', 'q12d[{q12d_7}].q12d_grid']},
                          +...           {'q12_8': ['q12a[{q12a_8}].q12a_grid', 'q12b[{q12b_8}].q12b_grid',
                          +...                      'q12c[{q12c_8}].q12c_grid', 'q12d[{q12d_8}].q12d_grid']},
                          +...           {'q12_9': ['q12a[{q12a_9}].q12a_grid', 'q12b[{q12b_9}].q12b_grid',
                          +...                      'q12c[{q12c_9}].q12c_grid', 'q12d[{q12d_9}].q12d_grid']},
                          +...           {'q12_10': ['q12a[{q12a_10}].q12a_grid', 'q12b[{q12b_10}].q12b_grid',
                          +...                       'q12c[{q12c_10}].q12c_grid', 'q12d[{q12d_10}].q12d_grid']},
                          +...           {'q12_11': ['q12a[{q12a_11}].q12a_grid', 'q12b[{q12b_11}].q12b_grid',
                          +...                       'q12c[{q12c_11}].q12c_grid', 'q12d[{q12d_11}].q12d_grid']},
                          +...           {'q12_12': ['q12a[{q12a_12}].q12a_grid', 'q12b[{q12b_12}].q12b_grid',
                          +...                       'q12c[{q12c_12}].q12c_grid', 'q12d[{q12d_12}].q12d_grid']},
                          +...           {'q12_13': ['q12a[{q12a_13}].q12a_grid', 'q12b[{q12b_13}].q12b_grid',
                          +...                       'q12c[{q12c_13}].q12c_grid', 'q12d[{q12d_13}].q12d_grid']}]]
                          +
                          +
                          +

                          Can be also writen like this:

                          +
                          >>> for y in frange('1-13'):
                          +...     q_group = []
                          +...     for x in  ['a', 'b', 'c', 'd']:
                          +...         var = 'q12{}'.format(x)
                          +...         var_grid = var + '[{' +  var + '_{}'.format(y) + '}].' + var + '_grid'
                          +...         q_group.append(var_grid)
                          +...     mapper.append({'q12_{}'.format(y): q_group})
                          +
                          +
                          +

                          So the derotated dataset will lose its meta information about the +mask and only the columns q12_1 to q12_13 will be added. To +receive back the mask structure, use the method dataset.to_array():

                          +
                          >>> variables = [{'q12_1': u'label 1'},
                          +...              {'q12_2': u'label 2'},
                          +...              {'q12_3': u'label 3'},
                          +...              {'q12_4': u'label 4'},
                          +...              {'q12_5': u'label 5'},
                          +...              {'q12_6': u'label 6'},
                          +...              {'q12_7': u'label 7'},
                          +...              {'q12_8': u'label 8'},
                          +...              {'q12_9': u'label 9'},
                          +...              {'q12_10': u'label 10'},
                          +...              {'q12_11': u'label 11'},
                          +...              {'q12_12': u'label 12'},
                          +...              {'q12_13': u'label 13'}]
                          +>>> ds.to_array('qTP', variables, 'Var_name')
                          +
                          +
                          +

                          variables can also be a list of variable-names, then the mask-items +will be named by its belonging columns.

                          +

                          arrays included in other will keep their meta structure.

                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/_build/html/sites/release_notes/how_to_snippets/dimensions_comp.html b/docs/API/_build/html/sites/release_notes/how_to_snippets/dimensions_comp.html new file mode 100644 index 000000000..7d8170f02 --- /dev/null +++ b/docs/API/_build/html/sites/release_notes/how_to_snippets/dimensions_comp.html @@ -0,0 +1,538 @@ + + + + + + + + + + + DataSet Dimensions compatibility — Quantipy 0.1.3 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          + + + + +
                          + + + + + + +
                          +
                          + + + + + + +
                          + +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +

                          DataSet Dimensions compatibility

                          +

                          DTO-downloaded and Dimensions converted variable naming conventions are following +specific rules for array names and corresponding ìtems. DataSet +offers a compatibility mode for Dimensions scenarios and handles the proper +renaming automatically. Here is what you should know…

                          +
                          +

                          The compatibility mode

                          +

                          A DataSet will (by default) support Dimensions-like array naming for its connected data files when constructed. An array masks meta defintition +of a variable called q5 looking like this…:

                          +
                          {u'items': [{u'source': u'columns@q5_1', u'text': {u'en-GB': u'Surfing'}},
                          +  {u'source': u'columns@q5_2', u'text': {u'en-GB': u'Snowboarding'}},
                          +  {u'source': u'columns@q5_3', u'text': {u'en-GB': u'Kite boarding'}},
                          +  {u'source': u'columns@q5_4', u'text': {u'en-GB': u'Parachuting'}},
                          +  {u'source': u'columns@q5_5', u'text': {u'en-GB': u'Cave diving'}},
                          +  {u'source': u'columns@q5_6', u'text': {u'en-GB': u'Windsurfing'}}],
                          + u'subtype': u'single',
                          + u'text': {u'en-GB': u'How likely are you to do each of the following in the next year?'},
                          + u'type': u'array',
                          + u'values': u'lib@values@q5'}
                          +
                          +
                          +

                          …will be converted into its “Dimensions equivalent” as per:

                          +
                          >>> dataset = qp.DataSet(name_data, dimensions_comp=True)
                          +>>> dataset.read_quantipy(path_data+name_data, path_data+name_data)
                          +DataSet: ../Data/Quantipy/Example Data (A)
                          +rows: 8255 - columns: 75
                          +Dimensions compatibilty mode: True
                          +
                          +
                          +
                          >>> dataset.masks()
                          +['q5.q5_grid', 'q6.q6_grid', 'q7.q7_grid']
                          +
                          +
                          +
                          >>> dataset._meta['masks']['q5.q5_grid']
                          +{u'items': [{u'source': 'columns@q5[{q5_1}].q5_grid',
                          +   u'text': {u'en-GB': u'Surfing'}},
                          +  {u'source': 'columns@q5[{q5_2}].q5_grid',
                          +   u'text': {u'en-GB': u'Snowboarding'}},
                          +  {u'source': 'columns@q5[{q5_3}].q5_grid',
                          +   u'text': {u'en-GB': u'Kite boarding'}},
                          +  {u'source': 'columns@q5[{q5_4}].q5_grid',
                          +   u'text': {u'en-GB': u'Parachuting'}},
                          +  {u'source': 'columns@q5[{q5_5}].q5_grid',
                          +   u'text': {u'en-GB': u'Cave diving'}},
                          +  {u'source': 'columns@q5[{q5_6}].q5_grid',
                          +   u'text': {u'en-GB': u'Windsurfing'}}],
                          + 'name': 'q5.q5_grid',
                          + u'subtype': u'single',
                          + u'text': {u'en-GB': u'How likely are you to do each of the following in the next year?'},
                          + u'type': u'array',
                          + u'values': 'lib@values@q5.q5_grid'}
                          +
                          +
                          +
                          +
                          +

                          Accessing and creating array data

                          +

                          Since new names are converted automatically by DataSet methods, there is +no need to write down the full (DTO-like) Dimensions array name when adding +new metadata. However, querying variables is always requiring the proper name:

                          +
                          >>> name, qtype, label = 'array_var', 'single', 'ARRAY LABEL'
                          +>>> cats =  ['A', 'B', 'C']
                          +>>> items = ['1', '2', '3']
                          +>>> dataset.add_meta(name, qtype, label, cats, items)
                          +
                          +
                          +
                          >>> dataset.masks()
                          +['q5.q5_grid', 'array_var.array_var_grid', 'q6.q6_grid', 'q7.q7_grid']
                          +
                          +
                          +
                          >>> dataset.meta('array_var.array_var_grid')
                          +single                                                                   items item texts  codes texts missing
                          +array_var.array_var_grid: ARRAY LABEL
                          +1                                      array_var[{array_var_1}].array_var_grid          1      1     A    None
                          +2                                      array_var[{array_var_2}].array_var_grid          2      2     B    None
                          +3                                      array_var[{array_var_3}].array_var_grid          3      3     C    None
                          +
                          +
                          +
                          >>> dataset['array_var.array_var_grid'].head(5)
                          +   array_var[{array_var_1}].array_var_grid  array_var[{array_var_2}].array_var_grid  array_var[{array_var_3}].array_var_grid
                          +0                                      NaN                                      NaN                                      NaN
                          +1                                      NaN                                      NaN                                      NaN
                          +2                                      NaN                                      NaN                                      NaN
                          +3                                      NaN                                      NaN                                      NaN
                          +4                                      NaN                                      NaN                                      NaN
                          +
                          +
                          +

                          As can been seen above, both the masks name as well as the array item +elements are being properly converted to match DTO/Dimensions +conventions.

                          +

                          When using rename(), copy() or transpose(), the same behaviour +applies:

                          +
                          >>> dataset.rename('q6.q6_grid', 'q6new')
                          +>>> dataset.masks()
                          +['q5.q5_grid', 'array_var.array_var_grid', 'q6new.q6new_grid', 'q7.q7_grid']
                          +
                          +
                          +
                          >>> dataset.copy('q6new.q6new_grid', suffix='q6copy')
                          +>>> dataset.masks()
                          +['q5.q5_grid', 'q6new_q6copy.q6new_q6copy_grid', 'array_var.array_var_grid', 'q6new.q6new_grid', 'q7.q7_grid']
                          +
                          +
                          +
                          >>> dataset.transpose('q6new_q6copy.q6new_q6copy_grid')
                          +>>> dataset.masks()
                          +['q5.q5_grid', 'q6new_q6copy_trans.q6new_q6copy_trans_grid', 'q6new_q6copy.q6new_q6copy_grid', 'array_var.array_var_grid', 'q6new.q6new_grid', 'q7.q7_grid']
                          +
                          +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/API/conf.py b/docs/API/conf.py index 9cab38b36..3187a551f 100644 --- a/docs/API/conf.py +++ b/docs/API/conf.py @@ -19,6 +19,8 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, "C:\\Users\\alt\\AppData\\Local\\Continuum\\Anaconda\\Lib\\site-packages\\quantipy") +sys.path.insert(0, "C:\\Users\\kmue\\Desktop\\quantipy") + # -- General configuration ------------------------------------------------ diff --git a/docs/API/sites/release_notes/01_latest.rst b/docs/API/sites/release_notes/01_latest.rst index 7a12ce56b..c614f5647 100644 --- a/docs/API/sites/release_notes/01_latest.rst +++ b/docs/API/sites/release_notes/01_latest.rst @@ -2,9 +2,106 @@ :maxdepth: 5 :includehidden: -=============== -Upcoming (July) -=============== +==================== +Upcoming (September) +==================== + +**New**: ``DataSet.min_value_count()`` + +A new wrapper for ``DataSet.hiding()`` is included. All values are hidden, +that have less counts than the included number ``min``. +The used data can be weighted or filtered using the parameters ``weight`` and +``condition``. + +Usage as Batch method: +``Batch.min_value_count()`` without the parameters ``weight`` and +``condition`` automatically grabs ``Batch.weights[0]`` and ``Batch.filter`` +to calculate low value counts. + +"""" + +**New**: Prevent weak duplicated in data + +As Python is case sensitive it is possible to have two or more variables with +the same name, but in lower- and uppercases. Most other software do not support +that, so a warning is shown if a weak dupe is created. Additionally +``Dataset.write_dimensions()`` performs auto-renaming is weak dupes are detected. + +"""" + +**New**: Prevent single-cat delimited sets + +``DataSet.add_meta(..., qtype='delimited set', categories=[...], ...)`` +automatically switches ``qtype`` to single if only one category is defined. +``DataSet.convert(name, 'single')`` allows conversion from ``delimited set`` to +``single`` if the variable has only one category. +``DataSet.repair()`` and ``DataSt.remove_values()`` convert delimited sets +automatically to singles if only one category is included. + +"""" + +**Update**: merge warnings + merging delimites sets + +Warnings in ``hmerge()`` and ``vmerge()`` are updated. If a column exists in +the left and the right dataset, the type is compared. Some type inconsistencies +are allowed, but return a warning, while others end up in a raise. + +delimited sets in ``vmerge()``: + +If a column is a delimited set in the left dataset, but a single, int or float +in the right dataset, the data of the right column is converted into a delimited +set. + +delimited sets in ``hmerge(...merge_existing=None)``: + +For the hmerge a new parameter ``merge_existing`` is included, which can be +``None``, a list of variable-names or ``'all'``. + +If delimited sets are included in left and right dataset: + +* ``merge_existing=None``: Only meta is adjusted. Data is untouched (left data +is taken). +* ``merge_existing='all'``: Meta and data are merged for all delimited sets, +that are included in both datasets. +* ``merge_existing=[variable-names]``: Meta and data are merged for all +delimited sets, that are listed and included in both datasets. + +"""" + +**Update**: encoding in ``DataSet.get_batch(name)`` + +The method is not that encoding sensitive anymore. It returns the depending +``Batch``, no matter if ``'...'``, ``u'...'`` or ``'...'.decode('utf8')`` is +included as name. + +"""" + +**Update**: warning in weight engine + +Missing codes in the sample are only alerted, if the belonging target is not 0. + +"""" + +**Update**: ``DataSet.to_array(..., variables, ...)`` + +Duplicated vars in ``variables`` are not allowed anymore, these were causing +problems in the ChainManager class. + +"""" + +**Update**: ``Batch.add_open_ends()`` + +Method raises an error if no vars are included in ``oe`` and ``break_by``. The +empty dataframe was causing issues in the ChainManager class. + +"""" + +**Update**: ``Batch.extend_x()`` + +The method automatically checks if the included variables are arrays and adds +them to ``Batch.summaries`` if they are included yet. + +"""" =================== @@ -14,7 +111,7 @@ Latest (04/06/2018) **New**: Additional variable (names) "getter"-like and resolver methods * ``DataSet.created()`` -* ``DataSet.find(str_tags=None, suffixed=False)`` +* ``DataSet.find(str_tags=None, suffixed=False)`` * ``DataSet.names()`` * ``DataSet.resolve_name()`` From 0a2aba825e009d1602288032f13e8a6bb2f1fdca Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Fri, 21 Sep 2018 16:01:16 +0200 Subject: [PATCH 474/733] bugfix: change dataset ref to self --- quantipy/core/dataset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index becb2fd15..378a1f8f9 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2288,11 +2288,11 @@ def _clean_datafile_set(self): if not variable in self: file_list.remove(item) elif collection == 'masks': - for s in dataset._get_source_ref(variable): + for s in self._get_source_ref(variable): while s in file_list: file_list.remove(s) - elif dataset._is_array_item(variable): - parent = dataset.parents(variable)[0] + elif self._is_array_item(variable): + parent = self.parents(variable)[0] if not parent in file_list: idx = file_list.index(item) file_list[idx] = parent From 7ecf433aaed65c1e47270e261ff3061ce0564660 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Tue, 25 Sep 2018 09:15:26 +0200 Subject: [PATCH 475/733] rewrite of all get methods in Class PptxDataFrame --- quantipy/sandbox/pptx/PptxChainClass.py | 213 ++++++++++++------------ 1 file changed, 102 insertions(+), 111 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index c3a6dfb20..101640ef1 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -234,7 +234,20 @@ def to_table(self, decimals=2, pct_decimals=2): return self - def select_categories(self,categories): + def _select_categories(self,categories): + """ + Returns a copy of self.df only including the categories requested + + Parameters + ---------- + categories : list + A list of ints specifying the categories from self.df to return + + Returns + ------- + pptx_df_copy : PptxDataFrame + + """ if self.array_style == -1: df_copy=self.df.iloc[categories] @@ -247,24 +260,45 @@ def select_categories(self,categories): return pptx_df_copy - def get_propstest(self): + def get_means(self): """ - Return a copy of the PptxDataFrame only containing sig testing type categories + Return a copy of the PptxDataFrame only containing mean type categories Returns ------- A PptxDataFrame instance """ - row_list = get_indexes_from_list(self.cell_items, 'is_propstest', exact=False) - #dont_want = get_indexes_from_list(self.cell_items, ['is_net', 'net', 'is_c_pct_sum'], exact=False) - #not_net = get_indexes_from_list(self.cell_items, ['normal', 'expanded'], exact=False) - #for x in dont_want: - # if x in row_list and x not in not_net: - # row_list.remove(x) + def get_nets(self): + """ + Return a copy of the PptxDataFrame only containing net type categories + + Returns + ------- + A PptxDataFrame instance + + """ + + def get_cpct(self): + """ + Return a copy of the PptxDataFrame only containing column percentage categories + + Returns + ------- + A PptxDataFrame instance + + """ + + def get_propstest(self): + """ + Return a copy of the PptxDataFrame only containing sig testing type categories + + Returns + ------- + A PptxDataFrame instance - return self.select_categories(row_list) + """ def get_stats(self): """ @@ -275,23 +309,39 @@ def get_stats(self): A PptxDataFrame instance """ - row_list = get_indexes_from_list(self.cell_items, 'is_stat', exact=False) - #dont_want = get_indexes_from_list(self.cell_items, ['is_net', 'net', 'is_c_pct_sum'], exact=False) - #not_net = get_indexes_from_list(self.cell_items, ['normal', 'expanded'], exact=False) - #for x in dont_want: - # if x in row_list and x not in not_net: - # row_list.remove(x) + def _get_propstest_index(self): + """ + Return a list of index numbers from self.cell_items of type 'is_propstest' - return self.select_categories(row_list) + Returns + ------- + row_list : list - def get_cpct(self): """ - Return a copy of the PptxDataFrame only containing column percentage categories + row_list = get_indexes_from_list(self.cell_items, 'is_propstest', exact=False) + return row_list + + def _get_stats_index(self): + """ + Return a list of index numbers from self.cell_items of type 'is_stat' Returns ------- - A PptxDataFrame instance + row_list : list + + """ + row_list = get_indexes_from_list(self.cell_items, 'is_stat', exact=False) + return row_list + + def _get_cpct_index(self): + """ + Return a list of index numbers from self.cell_items of type 'is_c_pct' and not types + 'is_net', 'net', 'is_c_pct_sum' + + Returns + ------- + row_list : list """ @@ -303,15 +353,16 @@ def get_cpct(self): if x in row_list and x not in not_net: row_list.remove(x) - return self.select_categories(row_list) + return row_list - def get_nets(self): + def _get_nets_index(self): """ - Return a copy of the PptxDataFrame only containing net type categories + Return a list of index numbers from self.cell_items of types 'is_net' or 'net' and not types + 'is_propstest', 'calc', 'normal', 'is_c_pct_sum', 'is_counts', 'expanded' Returns ------- - A PptxDataFrame instance + row_list : list """ @@ -324,15 +375,16 @@ def get_nets(self): if x in row_list: row_list.remove(x) - return self.select_categories(row_list) + return row_list - def get_means(self): + def _get_means_index(self): """ - Return a copy of the PptxDataFrame only containing mean type categories + Return a list of index numbers from self.cell_items of type 'is_mean' and not type + 'is_meanstest' Returns ------- - A PptxDataFrame instance + row_list : list """ @@ -343,15 +395,15 @@ def get_means(self): if x in row_list: row_list.remove(x) - return self.select_categories(row_list) + return row_list - def get(self, cell_items_request): + def get(self, cell_types, sort=False): """ Method to get specific elements from chains dataframe Parameters ---------- - cell_items_request : str + cell_types : str A string of comma separated cell types to return. Available types are 'c_pct, net, mean' Returns @@ -359,97 +411,36 @@ def get(self, cell_items_request): A PptxDataFrame instance """ - method_map = {'c_pct': self.get_cpct, - 'pct': self.get_cpct, - 'net': self.get_nets, - 'nets': self.get_nets, - 'mean': self.get_means, - 'means': self.get_means, - 'test': self.get_propstest, - 'tests': self.get_propstest, - 'stats': self.get_stats, - 'stat': self.get_stats} + method_map = {'c_pct': self._get_cpct_index, + 'pct': self._get_cpct_index, + 'net': self._get_nets_index, + 'nets': self._get_nets_index, + 'mean': self._get_means_index, + 'means': self._get_means_index, + 'test': self._get_propstest_index, + 'tests': self._get_propstest_index, + 'stats': self._get_stats_index, + 'stat': self._get_stats_index} # TODO Add methods for 'stddev', 'min', 'max', 'median', 't_means' - available_cell_items = set(method_map.keys()) - if isinstance(cell_items_request, basestring): - cell_items_request = re.sub(' +', '', cell_items_request) - cell_items_request = cell_items_request.split(',') - value_test = set(cell_items_request).difference(available_cell_items) - if value_test: - raise ValueError("Cell type: {} is not an available cell type. \n Available cell types are {}".format(cell_items_request, available_cell_items)) - - list_of_dataframes = [] - cell_items = [] - - for cell_item in cell_items_request: - pptx_frame = method_map[cell_item]() - #if pptx_frame.df.empty: - # continue - list_of_dataframes.append(pptx_frame.df) - cell_items += pptx_frame.cell_items - - new_df=pd.concat(list_of_dataframes, axis=0 if self.array_style==-1 else 1) - - new_pptx_df = PptxDataFrame(new_df, cell_items, self.array_style, self.chart_type) - new_pptx_df.chart_type = auto_charttype(new_df, self.array_style) - - return new_pptx_df - - def get_kerstin(self, cell_types, sort=False): - """ - Method to get specific elements from chains dataframe - :param - cel_types: A comma separated list of cell types to return. Available types are 'c_pct,net' - sort: Sort the elements ascending or decending. Str 'asc', 'dsc' or False - :return: df_copy, a Pandas dataframe. Element types will be returned in the order they are requested - """ - # method_map = {'c_pct': self.get_cpct, - # 'net': self.get_nets} - # TODO Add methods for 'mean', 'stddev', 'min', 'max', 'median', 't_props', 't_means' - available_celltypes = ['c_pct', 'net'] # set(method_map.keys()) + available_cell_types = set(method_map.keys()) if isinstance(cell_types, basestring): cell_types = re.sub(' +', '', cell_types) cell_types = cell_types.split(',') - value_test = set(cell_types).difference(available_celltypes) + value_test = set(cell_types).difference(available_cell_types) if value_test: - msg = "Cell type: {} is not an available cell type.\n" - msg += "Available cell types are {}" - raise ValueError(msg.format(cell_types, available_celltypes)) - - req_ct = [] - exclude = ['normal', 'calc', 'is_propstest', 'is_c_pct_sum', 'is_counts'] - if 'c_pct' in cell_types: - req_ct.append('is_c_pct') - if 'net' in cell_types: - req_ct.extend(['is_net', 'net']) - else: - exclude.extend(['is_net', 'net']) + raise ValueError("Cell type: {} is not an available cell type. \n Available cell types are {}".format(cell_types, available_cell_types)) - row_list = get_indexes_from_list(self.cell_contents, req_ct, exact=False) - dont_want = get_indexes_from_list(self.cell_contents, exclude, exact=False) - row_list = [x for x in row_list if not x in dont_want] + cell_types_list = [] - if self.array_style == -1: - df_copy = self.iloc[row_list] - else: - df_copy = self.iloc[:, row_list] + for cell_type in cell_types: + cell_types_list.extend(method_map[cell_type]()) - new_df = self.make_copy(data=df_copy.values, index=df_copy.index, columns=df_copy.columns) - new_df.chart_type = auto_charttype(new_df, new_df.array_style) - cell_contents = new_df.cell_contents - new_df.cell_contents = [cell_contents[i] for i in row_list] + if sort: cell_types_list.sort() - # for cell_type in cell_types: - # frame = method_map[cell_type]() - # frames.append(frame) - # cell_contents += frame.cell_contents - # new_df=pd.concat(frames, axis=0 if self.array_style==-1 else 1) + new_pptx_df = self._select_categories(cell_types_list) - pptx_df = self.make_copy(data=new_df.values, index=new_df.index, columns=new_df.columns) - pptx_df.chart_type = auto_charttype(pptx_df, pptx_df.array_style) - pptx_df.cell_contents = cell_contents + return new_pptx_df - return pptx_df class PptxChain(object): """ From 10cf79b72a9344884b4423eb65dd9c5a7019ed34 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Tue, 25 Sep 2018 14:55:18 +0200 Subject: [PATCH 476/733] set PptxDataFrame.get(sort=True) as default --- quantipy/sandbox/pptx/PptxChainClass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 101640ef1..487f159a1 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -397,7 +397,7 @@ def _get_means_index(self): return row_list - def get(self, cell_types, sort=False): + def get(self, cell_types, sort=True): """ Method to get specific elements from chains dataframe From cbaf54d06f4a4a93a942f274f6fa45235b105c3e Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Tue, 25 Sep 2018 15:33:00 +0200 Subject: [PATCH 477/733] fix in stack: do not add factors if mean is created by othersource --- quantipy/core/stack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index fa6010f6c..394ef0e30 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2618,8 +2618,8 @@ def _add_factors(v, meta, values, args): ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) ds.from_stack(self, dk) - - self._add_factor_meta(ds, v, (rescale, drop, exclude)) + if not other_source: + self._add_factor_meta(ds, v, (rescale, drop, exclude)) view = qp.ViewMapper() From 764a30c7f9053093774d8dc19f1487fbab643f66 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 26 Sep 2018 10:08:38 +0200 Subject: [PATCH 478/733] In PptxDataFrame.get() rename parameter 'sort' to 'original_order' --- quantipy/sandbox/pptx/PptxChainClass.py | 167 ++++++++++++++++++------ 1 file changed, 130 insertions(+), 37 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 487f159a1..8b9fe6998 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -18,12 +18,19 @@ def float2String(input, ndigits=0): """ Round and converts the input, if int/float or list of, to a string. - :param - input: int/float or list of int/float - ndigits: number of decimals to round to - :return: - output: string or list of strings depeneding on the input + + Parameters + ---------- + input: int/float or list of int/float + ndigits: int + number of decimals to round to + + Returns + ------- + output: string or list of strings + depending on the input """ + output = input if not isinstance(input, list): output = [output] @@ -52,15 +59,20 @@ def uniquify(l): def strip_levels(df, rows=None, columns=None): """ Function that strips a MultiIndex DataFrame for specified row and column index - Can be used with all DP-systems - :param - rows: Int, default None - Row index to remove - columns: Int, default None - Column index to remove - :return: - df_strip: A pandas.Dataframe + + Parameters + ---------- + df: pandas.DataFrame + rows: int + Row index to remove, default None + columns: int + Column index to remove, default None + + Returns + ------- + df_strip: pandas.DataFrame The input dataframe stripped for specified levels + """ df_strip = df.copy() if rows is not None: @@ -79,13 +91,16 @@ def as_numeric(df): '%' to '' '-' to '0' '*' to '0' - Can be used with all DP-systems - :param - df: Pandas.Dataframe - A dataframe of any structure, also multiindex - :return: - df: A pandas.Dataframe - dataframe with values as float + + Parameters + ---------- + df : pandas.DataFrame + + Returns + ------- + pandas.DataFrame + with values as float + """ if not df.values.dtype in ['float64', 'int64']: @@ -97,9 +112,16 @@ def as_numeric(df): def is_grid_slice(chain): """ Returns True if chain is a grid slice - :param - chain: the chain instance - :return: True id grid slice + + Parameters + ---------- + chain: quantipy.Chain + + Returns + ------- + bool + True if grid slice + """ pattern = '\[\{.*?\}\].' found = re.findall(pattern, chain.name) @@ -111,12 +133,26 @@ def get_indexes_from_list(lst, find, exact=True): """ Helper function that search for element in a list and returns a list of indexes for element match - E.g. get_indexes_from_list([1,2,3,1,5,1], 1) returns [0,3,5] - :param - lst: a list - find: the element to find. Can be a list - exact: If False the index are returned if find in value - :return: a list of ints + E.g. + get_indexes_from_list([1,2,3,1,5,1], 1) returns [0,3,5] + get_indexes_from_list(['apple','banana','orange','lemon'], 'orange') -> returns [2] + get_indexes_from_list(['apple','banana','lemon',['orange', 'peach']], 'orange') -> returns [] + get_indexes_from_list(['apple','banana','lemon',['orange', 'peach']], ['orange'], False) -> returns [3] + + Parameters + ---------- + lst: list + The list to look in + find: any + the element to find, can be a list + exact: bool + If False then index are returned if find in lst-item otherwise + only if find = lst-item + + Returns + ------- + list of int + """ if exact == True: return [index for index, value in enumerate(lst) if value == find] @@ -131,10 +167,21 @@ def auto_charttype(df, array_style, max_pie_elms=MAX_PIE_ELMS): """ Auto suggest chart type based on dataframe analysis TODO Move this to Class PptxDataFrame() - :param - df: a Pandas Dataframe, not multiindex - array_style: array_style as returned from Chain Class - :return: charttype ('bar_clustered', 'bar_stacked', 'bar_stacked_100', 'pie') + + Parameters + ---------- + df: pandas.DataFrame + Not multiindex + array_style: int + array_style as returned from Chain Class + max_pie_elms: int + Max number of elements in Pie chart + + Returns + ------- + str + One of charttypes ('bar_clustered', 'bar_stacked_100', 'pie') + """ if array_style == -1: # Not array summary chart_type = 'bar_clustered' @@ -151,7 +198,16 @@ def auto_charttype(df, array_style, max_pie_elms=MAX_PIE_ELMS): def fill_gaps(l): """ Return l replacing empty strings with the value from the previous position. + + Parameters + ---------- + l: list + + Returns + ------- + list """ + lnew = [] for i in l: if i == '': @@ -164,7 +220,16 @@ def fill_gaps(l): def fill_index_labels(df): """ Fills in blank labels in the second level of df's multi-level index. + + Parameters + ---------- + df: pandas.DataFrame + + Returns + ------- + pandas.DataFrame """ + _0, _1 = zip(*df.index.values.tolist()) _1new = fill_gaps(_1) dfnew = df.copy() @@ -175,17 +240,34 @@ def fill_index_labels(df): def fill_column_values(df, icol=0): """ Fills empty values in the targeted column with the value above it. + + Parameters + ---------- + df: pandas.DataFrame + icol: int + + Returns + ------- + pandas.DataFrame """ + v = df.iloc[:,icol].fillna('').values.tolist() vnew = fill_gaps(v) - dfnew = df.copy() + dfnew = df.copy() # type: pd.DataFrame dfnew.iloc[:,icol] = vnew return dfnew class PptxDataFrame(object): """ - Class for handling the dataframe to be charted + Class for handling the dataframe to be charted. + The class is instantiated from the class PptxChain and holds + the chains dataframe, flattened and ready for charting. + A series of get cell-types methods can be used to select specific cell-types. + + Attributes + + """ def __init__(self, dataframe, cell_items, array_style, chart_type): @@ -199,6 +281,17 @@ def __call__(self): return self.df def to_table(self, decimals=2, pct_decimals=2): + """ + + Parameters + ---------- + decimals + pct_decimals + + Returns + ------- + + """ df = self.df if df.empty: @@ -397,7 +490,7 @@ def _get_means_index(self): return row_list - def get(self, cell_types, sort=True): + def get(self, cell_types, original_order=True): """ Method to get specific elements from chains dataframe @@ -435,7 +528,7 @@ def get(self, cell_types, sort=True): for cell_type in cell_types: cell_types_list.extend(method_map[cell_type]()) - if sort: cell_types_list.sort() + if original_order: cell_types_list.sort() new_pptx_df = self._select_categories(cell_types_list) From 4814da1ca226e872104c0346b3cd5da8cd59c083 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 26 Sep 2018 11:48:25 +0200 Subject: [PATCH 479/733] rename PptxDataFrame attribute cell_items to cell_types --- quantipy/sandbox/pptx/PptxChainClass.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 8b9fe6998..2b7724372 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -265,14 +265,22 @@ class PptxDataFrame(object): the chains dataframe, flattened and ready for charting. A series of get cell-types methods can be used to select specific cell-types. - Attributes - + Attributes: + df: pandas.DataFrame + The actual dataframe ready to use with PptxPainter + array_style: int + Array style as given by quantipy.chain.array_style + cell_types: list + The dataframes cell types as given by quantipy.chain.contents + chart_type: str + Holds the chart type. Can be set when class is instantiated. + If not set a chart type will be auto set when using a get method. """ - def __init__(self, dataframe, cell_items, array_style, chart_type): + def __init__(self, dataframe, cell_types, array_style, chart_type=None): self.array_style = array_style - self.cell_items = cell_items + self.cell_items = cell_types self.df = dataframe # type: pd.DataFrame self.__frames = [] self.chart_type = chart_type @@ -348,7 +356,8 @@ def _select_categories(self,categories): df_copy = self.df.iloc[:,categories] pptx_df_copy = PptxDataFrame(df_copy,self.cell_items,self.array_style,self.chart_type) - pptx_df_copy.chart_type = auto_charttype(df_copy, self.array_style) + if self.chart_type is None: + pptx_df_copy.chart_type = auto_charttype(df_copy, self.array_style) pptx_df_copy.cell_items = [self.cell_items[i] for i in categories] return pptx_df_copy @@ -1189,7 +1198,7 @@ def prepare_dataframe(self): df.iloc[:, indexes] /= 100 # Make a PptxDataFrame instance - chart_df = PptxDataFrame(df,cell_contents,self.array_style,None) + chart_df = PptxDataFrame(df, cell_contents, self.array_style) # Choose a basic Chart type that will fit dataframe TODO Move this to init of Class PptxDataFrame chart_df.chart_type = auto_charttype(df, self.array_style) From 37991a42f7e32042437ffba7b08daa19d6f5a473 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 26 Sep 2018 12:19:07 +0200 Subject: [PATCH 480/733] drop attribute 'chart_type' in class PptxDataFrame --- quantipy/sandbox/pptx/PptxChainClass.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 2b7724372..a5e78248a 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -272,18 +272,14 @@ class PptxDataFrame(object): Array style as given by quantipy.chain.array_style cell_types: list The dataframes cell types as given by quantipy.chain.contents - chart_type: str - Holds the chart type. Can be set when class is instantiated. - If not set a chart type will be auto set when using a get method. """ - def __init__(self, dataframe, cell_types, array_style, chart_type=None): + def __init__(self, dataframe, cell_types, array_style): self.array_style = array_style self.cell_items = cell_types self.df = dataframe # type: pd.DataFrame self.__frames = [] - self.chart_type = chart_type def __call__(self): return self.df @@ -355,9 +351,7 @@ def _select_categories(self,categories): else: df_copy = self.df.iloc[:,categories] - pptx_df_copy = PptxDataFrame(df_copy,self.cell_items,self.array_style,self.chart_type) - if self.chart_type is None: - pptx_df_copy.chart_type = auto_charttype(df_copy, self.array_style) + pptx_df_copy = PptxDataFrame(df_copy,self.cell_items,self.array_style) pptx_df_copy.cell_items = [self.cell_items[i] for i in categories] return pptx_df_copy From 30c02ca22c822f3e3ac183aac0263dd770795d1f Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 26 Sep 2018 13:51:57 +0200 Subject: [PATCH 481/733] drop setting 'default_side_table['values_suffix'] = '%'' --- quantipy/sandbox/pptx/pptx_defaults.py | 1 - 1 file changed, 1 deletion(-) diff --git a/quantipy/sandbox/pptx/pptx_defaults.py b/quantipy/sandbox/pptx/pptx_defaults.py index 8643e3f2c..9540e0a62 100644 --- a/quantipy/sandbox/pptx/pptx_defaults.py +++ b/quantipy/sandbox/pptx/pptx_defaults.py @@ -130,7 +130,6 @@ default_side_table['top_member_cell_kwargs'] = default_cell_kwargs.copy() default_side_table['top_member_cell_kwargs']['vertical_alignment'] = 'middle' -default_side_table['values_suffix'] = '%' default_side_table['side_member_textframe_kwargs']['font_kwargs'] = default_font.copy() default_side_table['side_member_textframe_kwargs']['font_kwargs']['font_size'] = 8 From 380793385940ee99098e7bf610488697a9bfe7a1 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 26 Sep 2018 14:32:38 +0200 Subject: [PATCH 482/733] small change in how % sign is suffixed values in side_table --- quantipy/sandbox/pptx/PptxPainterClass.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index f7b556484..b883dea04 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -214,7 +214,9 @@ def queue_slide_items(self, pptx_chain, slide_items): if not pptx_frame().empty: side_table_draft = self.draft_side_table(pptx_frame()) pct_index = [index for index, value in enumerate(pptx_frame.cell_items) if 'is_c_pct' in value] - side_table_draft['values_suffix_columns'] = pct_index + if pct_index: + side_table_draft['values_suffix'] = '%' + side_table_draft['values_suffix_columns'] = pct_index self.queue_side_table(settings=side_table_draft) if slide_item.startswith('chart'): cell_items = slide_item.split(':')[1] From 24ac20543722aebea7fad5def3e356e33d2a3ed4 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Thu, 27 Sep 2018 09:26:00 +0200 Subject: [PATCH 483/733] just docs --- quantipy/sandbox/pptx/PptxChainClass.py | 256 +++++++++++++++--------- 1 file changed, 159 insertions(+), 97 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index a5e78248a..9dd84871c 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -265,17 +265,19 @@ class PptxDataFrame(object): the chains dataframe, flattened and ready for charting. A series of get cell-types methods can be used to select specific cell-types. - Attributes: - df: pandas.DataFrame - The actual dataframe ready to use with PptxPainter - array_style: int - Array style as given by quantipy.chain.array_style - cell_types: list - The dataframes cell types as given by quantipy.chain.contents + Parameters: + ---------- + df: pandas.DataFrame + The actual dataframe ready to use with PptxPainter + array_style: int + Array style as given by quantipy.chain.array_style + cell_types: list + The dataframes cell types as given by quantipy.chain.contents """ def __init__(self, dataframe, cell_types, array_style): + self.array_style = array_style self.cell_items = cell_types self.df = dataframe # type: pd.DataFrame @@ -286,14 +288,19 @@ def __call__(self): def to_table(self, decimals=2, pct_decimals=2): """ + Returns self.df formatted to be added to a table in a slide. + Basically just rounds values and if cell type = % then multiply values with 100 + # todo : should'nt be here, move to PptxPainter Parameters ---------- - decimals - pct_decimals - + decimals: int + Number of decimals for not percentage cell_types + pct_decimals: int + Number of decimals for percentage cell_types Returns ------- + self """ @@ -333,7 +340,7 @@ def to_table(self, decimals=2, pct_decimals=2): def _select_categories(self,categories): """ - Returns a copy of self.df only including the categories requested + Returns a copy of self.df having only the categories requested Parameters ---------- @@ -358,13 +365,14 @@ def _select_categories(self,categories): def get_means(self): """ - Return a copy of the PptxDataFrame only containing mean type categories + Return a copy of the PptxDataFrame containing only mean type categories Returns ------- - A PptxDataFrame instance + PptxDataFrame """ + return self.get('means') def get_nets(self): """ @@ -372,9 +380,11 @@ def get_nets(self): Returns ------- - A PptxDataFrame instance + PptxDataFrame """ + return self.get('net') + def get_cpct(self): """ @@ -382,9 +392,10 @@ def get_cpct(self): Returns ------- - A PptxDataFrame instance + PptxDataFrame """ + return self.get('c_pct') def get_propstest(self): """ @@ -392,9 +403,10 @@ def get_propstest(self): Returns ------- - A PptxDataFrame instance + PptxDataFrame """ + return self.get('tests') def get_stats(self): """ @@ -402,9 +414,10 @@ def get_stats(self): Returns ------- - A PptxDataFrame instance + PptxDataFrame """ + return self.get('stats') def _get_propstest_index(self): """ @@ -495,16 +508,23 @@ def _get_means_index(self): def get(self, cell_types, original_order=True): """ - Method to get specific elements from chains dataframe + Method to get specific cell types from chains dataframe. + Will return a copy of the PptxDataFrame instance containing only + the requested cell types. + Available types are 'c_pct, net, mean, test, stat' Parameters ---------- cell_types : str - A string of comma separated cell types to return. Available types are 'c_pct, net, mean' + A string of comma separated cell types to return. + original_order: Bool + Only relevant if more than one cell type is requested. + If True, cell types are returned in the same order as input dataframe. + If False, cell types will be returned in the order they are requested. Returns ------- - A PptxDataFrame instance + PptxDataFrame """ method_map = {'c_pct': self._get_cpct_index, @@ -540,16 +560,25 @@ def get(self, cell_types, original_order=True): class PptxChain(object): """ - This class is a wrapper around Chain class to prepare for PPTX charting + This class is a wrapper around Chain class to prepare for PPTX charting. + + Parameters + ---------- + chain: quantipy.sandbox.sandbox.Chain + is_varname_in_qtext: Bool + Set to True if question name is included in question text + crossbreak: str + Select a crossbreak to include in charts. Default is None + base_type: str + Select the base type to show in base descriptions: 'weighted' or 'unweighted' + decimals: int + Select the number of decimals to include from Chain.dataframe + verbose: Bool + """ def __init__(self, chain, is_varname_in_qtext=True, crossbreak=None, base_type='weighted', decimals=2, verbose=True): - """ - :param - chain: An instance of Chain class - is_varname_in_qtext: Is var name included in the painted chain dataframe? (False, True, 'full', 'ignore') - crossbreak: - """ + self._chart_type = None self._sig_test = None # type: list # is updated by ._select_crossbreak() self.crossbreak_qtext = None # type: str # is updated by ._select_crossbreak() @@ -568,7 +597,7 @@ def __init__(self, chain, is_varname_in_qtext=True, crossbreak=None, base_type=' self.is_grid_summary = True if chain.array_style in [0,1] else False self.crossbreak = self._check_crossbreaks(crossbreak) if crossbreak else [BASE_COL] self.x_key_short_name = self._get_short_question_name() - self.chain_df = self._select_crossbreak() + self.chain_df = self._select_crossbreak() # type: pd.DataFrame self.xbase_indexes = self._base_indexes() self.xbase_labels = ["Base"] if self.xbase_indexes is None else [x[0] for x in self.xbase_indexes] self.xbase_count = "" @@ -619,20 +648,77 @@ def sig_test(self): @property def chart_type(self): - if self._chart_type is None: - self._chart_type = self.chart_df.chart_type return self._chart_type @chart_type.setter def chart_type(self, chart_type): self._chart_type = chart_type + def _base_indexes(self): + """ + Finds all categories of type 'is_counts' and 'is_c_base' and then returns + a list of tuples holding (label, index, cell_content, value) for each base. + Method only used when instantiating Class. + Poppulates self.xbase_indexes + + Eg. [(u'Unweighted base', 0, ['is_counts', 'is_c_base'], 1003.0), + (u'Base', 1, ['weight_1', 'is_weighted', 'is_counts', 'is_c_base'], 1002.9999999398246)] + + Returns + ------- + list + + """ + cell_contents = self._chain.describe() + if self.array_style == 0: + row = min([k for k, va in cell_contents.items() + if any(pct in v for v in va for pct in PCT_TYPES)]) + cell_contents = cell_contents[row] + + # Find base rows + bases = get_indexes_from_list(cell_contents, BASE_ROW, exact=False) + skip = get_indexes_from_list(cell_contents, ['is_c_base_gross'], exact=False) + base_indexes = [idx for idx in bases if not idx in skip] or bases + + # Show error if no base elements found + if not base_indexes: + #msg = "No 'Base' element found, base size will be set to None" + #warnings.warn(msg) + return None + + cell_contents = [cell_contents[x] for x in base_indexes] + + if self.array_style == -1 or self.array_style == 1: + + xlabels = self._chain.dataframe.index.get_level_values(-1)[base_indexes].tolist() + base_counts = self._chain.dataframe.iloc[base_indexes, 0] + + else: + + xlabels = self._chain.dataframe.columns.get_level_values(-1)[base_indexes].tolist() + base_counts = self._chain.dataframe.iloc[0, base_indexes] + + return zip(xlabels, base_indexes, cell_contents, base_counts) + def select_base(self,base_type='weighted'): """ - Sets self.xbase_label and self.xbase_count - :param base_type: str - :return: None set self + Uses self.xbase_indexes to set + self.xbase_label, + self.xbase_count, + self.xbase_index + self.ybases + + Parameters + ---------- + base_type: str + String to define which base type to use: 'weighted' or 'unweighted' + + Returns + ------- + None, sets self + """ + if not self.xbase_indexes: msg = "No 'Base' element found" warnings.warn(msg) @@ -658,47 +744,58 @@ def select_base(self,base_type='weighted'): self.xbase_index = self.xbase_indexes[index[0]][1] self.ybases = self._get_y_bases() - def _base_indexes(self): - """ - Returns a list of label, index, cell_content and value of bases found in x keys. - :return: list + def _get_y_bases(self): """ + Retrieves the y-keys base label and base size from the dataframe. + If no crossbreak is requested the output is a list with one tuple, eg. [(u'Total', '1003')]. + If eg. 'gender' is selected as crossbreak the output is [(u'Female', '487'), (u'Male', '516')] - cell_contents = self._chain.describe() - if self.array_style == 0: - row = min([k for k, va in cell_contents.items() - if any(pct in v for v in va for pct in PCT_TYPES)]) - cell_contents = cell_contents[row] - - # Find base rows - bases = get_indexes_from_list(cell_contents, BASE_ROW, exact=False) - skip = get_indexes_from_list(cell_contents, ['is_c_base_gross'], exact=False) - base_indexes = [idx for idx in bases if not idx in skip] or bases + Only used in method select_base. - # Show error if no base elements found - if not base_indexes: - #msg = "No 'Base' element found, base size will be set to None" - #warnings.warn(msg) - return None + Returns + ------- + list + List of tuples [(base label, base size)] - cell_contents = [cell_contents[x] for x in base_indexes] + """ + base_index = self.xbase_index - if self.array_style == -1 or self.array_style == 1: + if not self.is_grid_summary: - xlabels = self._chain.dataframe.index.get_level_values(-1)[base_indexes].tolist() - base_counts = self._chain.dataframe.iloc[base_indexes, 0] + # Construct a list of tuples with (base label, base size, test letter) + base_values = self.chain_df.iloc[base_index, :].values.tolist() + base_values = np.around(base_values, decimals=self._decimals).tolist() + base_values = float2String(base_values) + base_labels = list(self.chain_df.columns.get_level_values('Values')) + if self._chain.sig_levels: + base_test = list(self.chain_df.columns.get_level_values('Test-IDs')) + bases = zip(base_labels, base_values, base_test) + else: + bases = zip(base_labels, base_values) - else: + else: # Array summary + # Find base columns - xlabels = self._chain.dataframe.columns.get_level_values(-1)[base_indexes].tolist() - base_counts = self._chain.dataframe.iloc[0, base_indexes] + # Construct a list of tuples with (base label, base size) + base_values = self.chain_df.T.iloc[base_index,:].values.tolist() + base_values = np.around(base_values, decimals=self._decimals).tolist() + base_values = float2String(base_values) + base_labels = list(self.chain_df.index.get_level_values(-1)) + bases = zip(base_labels, base_values) - return zip(xlabels, base_indexes, cell_contents, base_counts) + #print ybases + return bases def _index_map(self): """ - Map not painted index with painted index into a list of tuples (notpainted, painted) - :return: + Map not painted self._chain.dataframe.index with painted index into a list of tuples (notpainted, painted) + If grid summary, columns are + + [('All', u'Base'), (1, u'1 gang'), ('', u''), (2, u'2 gange'), ('', u''), (3, u'3 gange'), ('', u''), (4, u'4 gange'), ('', u''), (5, u'5 gange'), ('', u''), (6, u'Mere end 5 gange'), ('', u''), (7, u'Nej, har ikke v\xe6ret p\xe5 skiferie'), ('', u''), (8, u'Ved ikke'), ('', u''), ('sum', u'Totalsum')] + + Returns + ------- + """ if self._chain.painted: # UnPaint if painted self._chain.toggle_labels() @@ -1120,41 +1217,6 @@ def _is_base_row(self, row): return False return True - def _get_y_bases(self): - """ - Retrieves the base label and base size from the dataframe - :param chain: the chain instance - :return: ybases - list of tuples [(base label, base size)] - """ - - base_index = self.xbase_index - - if not self.is_grid_summary: - - # Construct a list of tuples with (base label, base size, test letter) - base_values = self.chain_df.iloc[base_index, :].values.tolist() - base_values = np.around(base_values, decimals=self._decimals).tolist() - base_values = float2String(base_values) - base_labels = list(self.chain_df.columns.get_level_values('Values')) - if self._chain.sig_levels: - base_test = list(self.chain_df.columns.get_level_values('Test-IDs')) - bases = zip(base_labels, base_values, base_test) - else: - bases = zip(base_labels, base_values) - - else: # Array summary - # Find base columns - - # Construct a list of tuples with (base label, base size) - base_values = self.chain_df.T.iloc[base_index,:].values.tolist() - base_values = np.around(base_values, decimals=self._decimals).tolist() - base_values = float2String(base_values) - base_labels = list(self.chain_df.index.get_level_values(-1)) - bases = zip(base_labels, base_values) - - #print ybases - return bases - def prepare_dataframe(self): """ Prepares the dataframe for charting, that is takes self.chain_df and From b5b5d7de95a27fb10ea6d955da1f042bb38960ee Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 27 Sep 2018 10:19:04 +0200 Subject: [PATCH 484/733] quick bugfix in batch.extend_x --- quantipy/core/batch.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index dce2736d5..a2dad9ae2 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -466,12 +466,14 @@ def extend_x(self, ext_xks): raise KeyError('{} is not included.'.format(pos)) elif not v in self.xks: self.xks.insert(self.xks.index(pos), v) + if self.is_array(v) and not v in self.summaries: + self.summaries.append(v) elif not self.var_exists(x): raise KeyError('{} is not included.'.format(x)) elif x not in self.xks: self.xks.extend(self.unroll(x, both='all')) - if self.is_array(x) and not x in self.summaries: - self.summaries.append(x) + if self.is_array(x) and not x in self.summaries: + self.summaries.append(x) self._update() return None From 3a17f44270c531dd3576b3f1837838e6f6aa69fe Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 27 Sep 2018 10:34:39 +0200 Subject: [PATCH 485/733] add warning if sorting weight differs from view-weight --- quantipy/core/rules.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 46d0f5394..1ff7cf7e2 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -4,6 +4,7 @@ import copy import quantipy as qp import numpy as np +import warnings class Rules(object): @@ -100,6 +101,8 @@ def _get_sort_weight(self): if expanded_nets_w and not sort_weight in expanded_nets_w: sort_weight = expanded_nets_w[0] return sort_weight + else: + None # ------------------------------------------------------------------------ # display @@ -201,7 +204,7 @@ def get_slicer(self): f = self._get_frequency_via_stack(col_key, axis, self._sort_weight) # get df for hiding + slicing else: - f = self._get_frequency_via_stack(col_key, axis, '') + f = self._get_frequency_via_stack(col_key, axis, None) # get rules slicer f = f.T if self.array_summary and axis == 1 else f @@ -213,7 +216,14 @@ def get_slicer(self): return None def _get_frequency_via_stack(self, col, axis, weight): - vk = 'x|f|:||{}|counts'.format(weight) + if weight is None: + vk = 'x|f|:|||counts' + else: + vk = 'x|f|:||{}|counts'.format(weight) + view_weight = self.view_name.split('|')[-2] + if not weight == view_weight: + msg = "\n{}: view-weight and weight to sort on differ ('{}' vs '{}')\n" + warnings.warn(msg.format(col, view_weight, weight or None)) try: if self.transposed_summary: f = self.link_base['@'][col][vk].dataframe.T From 232f318a0de8097f67e24dbb1c52e0c3a0af9acc Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 27 Sep 2018 10:51:07 +0200 Subject: [PATCH 486/733] modify weight warning in sorting --- quantipy/core/rules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 1ff7cf7e2..f4c31cde2 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -221,7 +221,9 @@ def _get_frequency_via_stack(self, col, axis, weight): else: vk = 'x|f|:||{}|counts'.format(weight) view_weight = self.view_name.split('|')[-2] - if not weight == view_weight: + link_weights = [k.split('|')[-2] for k in self.link.keys() + if not 'base' in k.split('|')[-1]] + if not (weight == view_weight or len(link_weights)>1): msg = "\n{}: view-weight and weight to sort on differ ('{}' vs '{}')\n" warnings.warn(msg.format(col, view_weight, weight or None)) try: From d3f5846d31a93b5f08eff161b1963d38dbea6a47 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Thu, 27 Sep 2018 10:55:38 +0200 Subject: [PATCH 487/733] make link_weights to a set --- quantipy/core/rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index f4c31cde2..a30461f49 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -221,8 +221,8 @@ def _get_frequency_via_stack(self, col, axis, weight): else: vk = 'x|f|:||{}|counts'.format(weight) view_weight = self.view_name.split('|')[-2] - link_weights = [k.split('|')[-2] for k in self.link.keys() - if not 'base' in k.split('|')[-1]] + link_weights = set([k.split('|')[-2] for k in self.link.keys() + if not 'base' in k.split('|')[-1]]) if not (weight == view_weight or len(link_weights)>1): msg = "\n{}: view-weight and weight to sort on differ ('{}' vs '{}')\n" warnings.warn(msg.format(col, view_weight, weight or None)) From f77e5393c9c3ea11898fc13de9021de7fff1e418 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Thu, 27 Sep 2018 15:29:55 +0200 Subject: [PATCH 488/733] bugfix when repainting a chain --- quantipy/sandbox/sandbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index ad8f9e415..5b14ff603 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3083,6 +3083,7 @@ def _get_level_1(self, levels, text_keys, display, axis, bases): tk_transl = text_keys[axis] else: tk_transl = self._default_text + c_text = copy.deepcopy(self._custom_texts) if self._custom_texts else {} for i, value in enumerate(levels[1]): if str(value).startswith('#pad'): level_1_text.append(value) @@ -3099,8 +3100,8 @@ def _get_level_1(self, levels, text_keys, display, axis, bases): text = self._specify_base(i, text_keys[axis], bases) else: text = self._transl[tk_transl][value] - if self._custom_texts and value in self._custom_texts: - add_text = self._custom_texts[value].pop(0) + if value in c_text: + add_text = c_text[value].pop(0) text = '{} {}'.format(text, add_text) level_1_text.append(text) elif value == 'All (eff.)': From 8a37e77bdf63c4b2c7aecbafefcb16021991fc4e Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Fri, 28 Sep 2018 10:15:42 +0200 Subject: [PATCH 489/733] another change for warning for sorting on other weight --- quantipy/core/rules.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index a30461f49..8ab4e54f7 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -221,9 +221,9 @@ def _get_frequency_via_stack(self, col, axis, weight): else: vk = 'x|f|:||{}|counts'.format(weight) view_weight = self.view_name.split('|')[-2] - link_weights = set([k.split('|')[-2] for k in self.link.keys() - if not 'base' in k.split('|')[-1]]) - if not (weight == view_weight or len(link_weights)>1): + link_weights = [k.split('|')[-2] for k in self.link.keys() + if not 'base' in k.split('|')[-1]] + if not (weight == view_weight or weight not in link_weights): msg = "\n{}: view-weight and weight to sort on differ ('{}' vs '{}')\n" warnings.warn(msg.format(col, view_weight, weight or None)) try: From d9054f548faedc28bb8d315433f4ba4bc1752aa5 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 28 Sep 2018 12:41:21 +0200 Subject: [PATCH 490/733] Doc strings --- quantipy/sandbox/pptx/PptxChainClass.py | 129 +++++++++++++++++++----- 1 file changed, 103 insertions(+), 26 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 9dd84871c..6e170611c 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -750,7 +750,7 @@ def _get_y_bases(self): If no crossbreak is requested the output is a list with one tuple, eg. [(u'Total', '1003')]. If eg. 'gender' is selected as crossbreak the output is [(u'Female', '487'), (u'Male', '516')] - Only used in method select_base. + Only used in method select_base to poppulate self.ybases. Returns ------- @@ -788,13 +788,19 @@ def _get_y_bases(self): def _index_map(self): """ - Map not painted self._chain.dataframe.index with painted index into a list of tuples (notpainted, painted) - If grid summary, columns are + Map not painted self._chain.dataframe.index with painted index into + a list of tuples (notpainted, painted). + If grid summary, self._chain.dataframe.columns are map'ed instead. - [('All', u'Base'), (1, u'1 gang'), ('', u''), (2, u'2 gange'), ('', u''), (3, u'3 gange'), ('', u''), (4, u'4 gange'), ('', u''), (5, u'5 gange'), ('', u''), (6, u'Mere end 5 gange'), ('', u''), (7, u'Nej, har ikke v\xe6ret p\xe5 skiferie'), ('', u''), (8, u'Ved ikke'), ('', u''), ('sum', u'Totalsum')] + Example: + [('All', u'Base'), (1, u'Yes'), ('', u''), (2, u'No'), ('', u''), (8, u'Dont know'), + ('', u''), ('sum', u'Totalsum')] + + Only used to poppulate self.index_map in __init__ Returns ------- + list """ if self._chain.painted: # UnPaint if painted @@ -814,10 +820,16 @@ def _index_map(self): def _select_crossbreak(self): """ - Takes self._chain.dataframe and returns only the columns stated in self.crossbreak - :return: - """ + Takes self._chain.dataframe and returns a copy with only the columns + stated in self.crossbreak. + + Only used to poppulate self.chain_df in __init__. + + Returns + ------- + pd.DataFrame + """ cell_items = self._chain.cell_items.split('_') if not self.is_grid_summary: @@ -866,18 +878,43 @@ def _select_crossbreak(self): @property def ybase_values(self): + """ + Returns a list with y base values picked from self.ybases. + + Returns + ------- + list + + """ if not hasattr(self, "_ybase_values"): self._ybase_values=[x[1] for x in self.ybases] return self._ybase_values @property def ybase_value_labels(self): + """ + Returns a list with y base labels picked from self.ybases. + + Returns + ------- + list + + """ if not hasattr(self, "_ybase_value_labels"): self._ybase_value_labels=[x[0] for x in self.ybases] return self._ybase_value_labels @property def ybase_test_labels(self): + """ + Returns a list with y base test labels picked from self.ybases. + Eg. ['A', 'B'] + Returns + ------- + list + + """ + if not hasattr(self, "_ybase_test_labels"): if self.is_grid_summary: self._ybase_test_labels = None @@ -886,7 +923,25 @@ def ybase_test_labels(self): return self._ybase_test_labels def add_test_letter_to_column_labels(self, sep=" ", prefix=None, circumfix='()'): + """ + Adds test letter to dataframe column labels. + + Parameters + ---------- + sep: str + A string to separate the column label from the test letter, default is a single space. + prefix: str + An optional prefix. + circumfix: str + A two char string used to enclose the test letter. + Default '()' + Returns + ------- + None + changes self.chain_df + + """ # Checking input if circumfix is None: circumfix = list(('',) * 2) @@ -916,23 +971,32 @@ def add_test_letter_to_column_labels(self, sep=" ", prefix=None, circumfix='()') def place_vals_in_labels(self, base_position=0, orientation='side', values=None, sep=" ", prefix="n=", circumfix="()", setup='if_differs'): """ - Takes values from a given column or row and inserts it to the df's row or column labels. - Can be used to insert base values in side labels for a grid summary - :param - base_position: Int, Default 0 - Which row/column to pick the base element from - orientation: Str: 'side' or 'column', Default 'side' - Add base to row or column labels. + Takes values from input list and adds them to self.chain_df's categories, + Meaning rows if grid summary, otherwise columns. + + Can be used to insert base values in side labels for a grid summary. + + Parameters + ---------- + base_position: for future usage + orientation: for future usage values: list - the list of values to insert + a list with same number of values as categories in self.chain_df sep: str - the string to use to separate the value from the label + A string to separate the categories from the insert, default is a single space. prefix: str - A string to insert as a prefix to the label + A prefix to add to the insert. Default 'n=' circumfix: str - A character couple to surround the value + A two char string used to enclose the insert. + Default '()' setup: str A string telling when to insert value ('always', 'if_differs', 'never') + + Returns + ------- + None + Changes self.chain_df + """ if setup=='never': return @@ -1001,14 +1065,27 @@ def base_text(self, base_text): def set_base_text(self, base_value_circumfix="()", base_label_suf=":", base_description_suf=" - ", base_value_label_sep=", ", base_label=None): """ - Returns the full base text made up of base_label, base_description and ybases, with some delimiters - :param - base_value_circumfix: chars to surround the base value - base_label_suf: char to put after base_label - base_description_suf: When more than one column, use this char after base_description - base_value_label_sep: char to separate column label, if more than one - base_label: str adhoc to use for base label - :return: + Returns the full base text made up of base_label, base_description and ybases, with some delimiters. + Setup is "base_label + base_description + base_value" + + Parameters + ---------- + base_value_circumfix: str + Two chars to surround the base value + base_label_suf: str + A string to add after the base label + base_description_suf: str + A string to add after the base_description + base_value_label_sep: str + A string to separate base_values if more than one + base_label: str + An optional string to use instead of self.xbase_label + + Returns + ------- + str + Sets self._base_text + """ # Checking input if base_value_circumfix is None: From eec888de1b26bddd3dfd102f3d0c442d753157f1 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 28 Sep 2018 13:59:22 +0200 Subject: [PATCH 491/733] more doc strings --- quantipy/sandbox/pptx/PptxChainClass.py | 85 +++++++++++++++++-------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 6e170611c..e95bbfb0c 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -566,7 +566,10 @@ class PptxChain(object): ---------- chain: quantipy.sandbox.sandbox.Chain is_varname_in_qtext: Bool - Set to True if question name is included in question text + Is question name is included in question text? + False: No question name included in question text + True: Question name included in question text, mask items has short question name included. + 'Full': Question name included in question text, mask items has full question name included. crossbreak: str Select a crossbreak to include in charts. Default is None base_type: str @@ -1173,9 +1176,18 @@ def set_base_text(self, base_value_circumfix="()", base_label_suf=":", base_desc def _check_crossbreaks(self, crossbreaks): """ - Checks for existence of the requested crossbreaks - :param crossbreaks: - :return: + Checks the crossbreaks input for duplicates and that crossbreak exist in the chain. + + Parameters + ---------- + crossbreaks: list + List of strings + + Returns + ------- + list + The crossbreaks list stripped for duplicates and not existing crossbreaks + """ if not isinstance(crossbreaks, list): crossbreaks = [crossbreaks] @@ -1196,10 +1208,13 @@ def _check_crossbreaks(self, crossbreaks): def _get_short_question_name(self): """ - Retrieves question name - :param - chain: the chain instance - :return: question_name (as string) + Retrieves 'short' question name. + Used in __init__ to poppulate self.x_key_short_name + + Returns + ------- + str + """ if not self.is_grid_summary: # Not grid summary if self.is_mask_item: # Is grid slice @@ -1223,16 +1238,17 @@ def _get_short_question_name(self): def get_question_text(self, include_varname=False): """ Retrieves the question text from the dataframe. - Assumes that self.chain_df has one of the following setups regarding question name self._var_name_in_qtext: - False: No question name included in question text - True: Question text included in question text, mask items has short question name included. - 'Full': Question text included in question text, mask items has full question name included. + If include_varname=True then the question text will be prefixed the var name. - :param - include_varname: Include question name in question text (bool) - :return: question_txt (as string) - """ + Parameters + ---------- + include_varname: Bool + + Returns + ------- + str + """ # Get variable name var_name = self.x_key_name if self.is_mask_item: @@ -1261,15 +1277,24 @@ def get_question_text(self, include_varname=False): def _mask_question_text(self, question_text): """ - Adds to a mask items question text the array question text - if not allready there - :param - question_text: the question text from the mask item - :return: - question_text appended the array question text + If chain is a mask item (a grid slice), then the parent question text + is added to question text unless already included. + + Final question text in the form "parent_question_text - mask_question_text" + + Only used in self.get_question_text(). + + Parameters + ---------- + question_text: str + + Returns + ------- + str + """ if self.source == "native": - meta=self._chain._meta + meta = self._chain._meta cols = meta['columns'] masks = meta['masks'] if self.is_mask_item: @@ -1283,11 +1308,15 @@ def _mask_question_text(self, question_text): def _is_base_row(self, row): """ - Return True if Row is a Base row - :param - row: The row to check (list) - :return: - True or False + Return True if Row is a Base row. + + Parameters + ---------- + row + + Returns + ------- + """ for item in BASE_ROW: if item not in row: From 3b45a0542daed6c703833c02efda918ff2210e66 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 28 Sep 2018 14:01:05 +0200 Subject: [PATCH 492/733] small refactor in method _mask_question_text --- quantipy/sandbox/pptx/PptxChainClass.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index e95bbfb0c..bbd09e1b4 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -1294,10 +1294,10 @@ def _mask_question_text(self, question_text): """ if self.source == "native": - meta = self._chain._meta - cols = meta['columns'] - masks = meta['masks'] if self.is_mask_item: + meta = self._chain._meta + cols = meta['columns'] + masks = meta['masks'] parent = cols[self.x_key_name]['parent'].keys()[0].split('@')[-1] m_text = masks[parent]['text'] text = m_text.get('x edit', m_text).get(meta['lib']['default text']) From 89dbe5ae0c71a6c6d3a0e8d311b869c3e5b53960 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 28 Sep 2018 14:03:15 +0200 Subject: [PATCH 493/733] delete unused internal method _is_base_row() --- quantipy/sandbox/pptx/PptxChainClass.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index bbd09e1b4..26e7cd43c 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -1306,23 +1306,6 @@ def _mask_question_text(self, question_text): return question_text - def _is_base_row(self, row): - """ - Return True if Row is a Base row. - - Parameters - ---------- - row - - Returns - ------- - - """ - for item in BASE_ROW: - if item not in row: - return False - return True - def prepare_dataframe(self): """ Prepares the dataframe for charting, that is takes self.chain_df and From 55437016e5415cf215bcd07fce58ac859f712054 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 28 Sep 2018 14:06:45 +0200 Subject: [PATCH 494/733] more doc strings --- quantipy/sandbox/pptx/PptxChainClass.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 26e7cd43c..8d4e03829 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -1308,11 +1308,15 @@ def _mask_question_text(self, question_text): def prepare_dataframe(self): """ - Prepares the dataframe for charting, that is takes self.chain_df and - removes all outer levels and prepares the dataframe for PptxPainter. - :return: copy of chain.dataframe containing only rows and cols that are to be charted - """ + Prepares self.chain_df for charting, that is removes all outer levels + and prepares the dataframe for PptxPainter. + + Returns + ------- + pd.DataFrame + An edited copy of self.chain_df + """ # Strip outer level df = strip_levels(self.chain_df, rows=0, columns=0) df = strip_levels(df, columns=1) From 77dcb2559c0f308124aedfc45b8cbc25b867da11 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Fri, 28 Sep 2018 16:10:49 +0200 Subject: [PATCH 495/733] tests for DimLabels and DimLabel --- tests/test_io_dimensions.py | 88 +++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/tests/test_io_dimensions.py b/tests/test_io_dimensions.py index f60bbdaab..1ecff862c 100644 --- a/tests/test_io_dimensions.py +++ b/tests/test_io_dimensions.py @@ -1,43 +1,53 @@ import unittest import os.path -import numpy -import pandas as pd -from pandas.util.testing import assert_frame_equal -import test_helper +import quantipy as qp import copy - -from collections import defaultdict, OrderedDict -from quantipy.core.stack import Stack -from quantipy.core.chain import Chain -from quantipy.core.link import Link -from quantipy.core.view_generators.view_mapper import ViewMapper -from quantipy.core.view_generators.view_maps import QuantipyViews -from quantipy.core.view import View -from quantipy.core.helpers import functions -from quantipy.core.helpers.functions import load_json - -class TestStackObject(unittest.TestCase): - - def setUp(self): - pass - - @classmethod - def tearDownClass(self): - pass - -if __name__ == '__main__': - unittest.main() - - - - - - - - - - - - - +from quantipy.core.tools.dp.dimensions.dimlabels import ( + qp_dim_languages, + DimLabels) + + +def _get_dataset(): + path = os.path.dirname(os.path.abspath(__file__)) + '/' + name = 'Example Data (A)' + casedata = '{}.csv'.format(name) + metadata = '{}.json'.format(name) + dataset = qp.DataSet(name, False) + dataset.set_verbose_infomsg(False) + dataset.set_verbose_errmsg(False) + dataset.read_quantipy(path+metadata, path+casedata) + return dataset + +def loop_for_text_objs(obj, func, name=None): + if isinstance(obj, dict): + for k, v in obj.items(): + if k == 'text' and isinstance(v, dict): + func(copy.deepcopy(v), name) + else: + loop_for_text_objs(v, func, k) + elif isinstance(obj, list): + for o in obj: + loop_for_text_objs(o, func, name) + +class TestDimLabels(unittest.TestCase): + + def _test_dimlabels(self, x, name): + d_labels = DimLabels(name, 'de-DE') + d_labels.add_text(x) + languages = {v: k for k, v in qp_dim_languages.items()} + for lab in d_labels.labels: + t1 = lab.default_lan == 'DEU' + if lab.labeltype: + t2 = lab.text == x[lab.labeltype].pop(languages[lab.language]) + if not x[lab.labeltype]: del x[lab.labeltype] + else: + t2 = lab.text == x.pop(languages[lab.language]) + self.assertTrue(t1 and t2) + self.assertEqual({}, x) + + def test_dimlabels(self): + dataset = _get_dataset() + dataset.set_variable_text('q1', 'test label', 'en-GB', 'x') + dataset.set_value_texts('q2b', dict(dataset.values('q2b')), 'sv-SE') + loop_for_text_objs(dataset._meta, self._test_dimlabels) From 32df5eb9693843f266ba1165ba83854fd821d884 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Mon, 1 Oct 2018 10:22:13 +0200 Subject: [PATCH 496/733] change warnings/ errrors for merge --- quantipy/core/tools/dp/prep.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/quantipy/core/tools/dp/prep.py b/quantipy/core/tools/dp/prep.py index d1e56316a..00fa32da0 100644 --- a/quantipy/core/tools/dp/prep.py +++ b/quantipy/core/tools/dp/prep.py @@ -1117,10 +1117,9 @@ def _compatible_types(left_column, right_column): 'float', 'delimited set', 'string', 'date', 'time', 'array'], 'float': [ 'delimited set', 'string', 'date', 'time', 'array'], - 'single': [ - 'delimited set', 'string', 'date', 'time', 'array'], + 'single': all_types, 'delimited set': [ - 'string', 'date', 'time', 'array'], + 'string', 'date', 'time', 'array', 'int', 'float'], 'string': [ 'int', 'float', 'single', 'delimited set', 'date', 'time', 'array'], 'date': [ @@ -1133,10 +1132,8 @@ def _compatible_types(left_column, right_column): 'single'], 'float': [ 'int', 'single'], - 'single': [ - 'int', 'float'], 'delimited set': [ - 'single', 'int', 'float'], + 'single'], 'string': [ 'boolean'] } From 44022a7ea8cc2a2012e86555ce50d141ff1ce296 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Mon, 1 Oct 2018 10:34:52 +0200 Subject: [PATCH 497/733] docs --- docs/API/_build/doctrees/environment.pickle | Bin 116428 -> 114286 bytes .../doctrees/sites/api_ref/Chain.doctree | Bin 15233 -> 15233 bytes .../doctrees/sites/api_ref/DataSet.doctree | Bin 854596 -> 854606 bytes .../sites/api_ref/QuantipyViews.doctree | Bin 62081 -> 62081 bytes .../doctrees/sites/api_ref/Rim_scheme.doctree | Bin 25865 -> 25865 bytes .../doctrees/sites/api_ref/View.doctree | Bin 39792 -> 39792 bytes .../sites/api_ref/quantify_engine.doctree | Bin 99705 -> 99710 bytes .../doctrees/sites/api_ref/stack.doctree | Bin 163788 -> 163788 bytes .../dataprocessing/00_overview.doctree | Bin 3286 -> 3284 bytes .../dataprocessing/03_inspection.doctree | Bin 48421 -> 48416 bytes .../sites/release_notes/01_latest.doctree | Bin 38119 -> 29670 bytes .../sites/release_notes/02_archive.doctree | Bin 291512 -> 308208 bytes .../sites/release_notes/01_latest.rst.txt | 118 +++++------------- .../sites/release_notes/02_archive.rst.txt | 85 +++++++++++++ docs/API/_build/html/genindex.html | 4 +- docs/API/_build/html/index.html | 4 +- docs/API/_build/html/objects.inv | Bin 2429 -> 2416 bytes docs/API/_build/html/search.html | 4 +- docs/API/_build/html/searchindex.js | 2 +- .../_build/html/sites/api_ref/00overview.html | 4 +- docs/API/_build/html/sites/api_ref/Chain.html | 5 +- .../_build/html/sites/api_ref/DataSet.html | 7 +- .../html/sites/api_ref/QuantipyViews.html | 5 +- .../_build/html/sites/api_ref/Rim_scheme.html | 9 +- docs/API/_build/html/sites/api_ref/View.html | 9 +- .../html/sites/api_ref/quantify_engine.html | 5 +- docs/API/_build/html/sites/api_ref/stack.html | 4 +- .../lib_doc/dataprocessing/00_overview.html | 5 +- .../lib_doc/dataprocessing/01_components.html | 5 +- .../sites/lib_doc/dataprocessing/02_io.html | 5 +- .../dataprocessing/02a_management.html | 5 +- .../lib_doc/dataprocessing/03_inspection.html | 5 +- .../html/sites/release_notes/00_overview.html | 12 +- .../html/sites/release_notes/01_latest.html | 98 +++++---------- .../html/sites/release_notes/02_archive.html | 68 +++++++++- docs/API/sites/release_notes/01_latest.rst | 118 +++++------------- docs/API/sites/release_notes/02_archive.rst | 85 +++++++++++++ 37 files changed, 388 insertions(+), 283 deletions(-) diff --git a/docs/API/_build/doctrees/environment.pickle b/docs/API/_build/doctrees/environment.pickle index 8d0bced2213989fc4a59f8f01b9962b26fa98ead..64f6f6e1fd64507960b9d796de797d3c3fd531c6 100644 GIT binary patch literal 114286 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 aj7F?^h4h?^SDevRZ1vVhdQ#W)vHuG}f4#2& literal 116428 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 YkZbIhoR{)CW*0kJyaOOlD>je$UoGGll>h($ diff --git a/docs/API/_build/doctrees/sites/api_ref/Chain.doctree b/docs/API/_build/doctrees/sites/api_ref/Chain.doctree index 0670b805ae4481efca638540e658b30853c3ec13..efe850331127fe2792919ca41dce9a227e44861a 100644 GIT binary patch delta 169 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

                          owI8DF KI;7Q8`uraK!e24MQuBJyV{zyI#0H%Dtb(?z!RP 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 z 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 diff --git a/docs/API/_build/doctrees/sites/api_ref/QuantipyViews.doctree b/docs/API/_build/doctrees/sites/api_ref/QuantipyViews.doctree index 8a34b101bc9c53bf30952db3df82df42a0123c7f..a881ddc1fe9c4f029fb70298150c03f81f946a1a 100644 GIT binary patch delta 159 zcmZpC%G~&rdBZ%m$*fHBoA|u32rkQA!@GG=^1_vZ4!4UdtB0c?ZvFW-JnMn?DGs zVwITuL4b2}x5x&6Y`P{N$l%<(F=Y`}otqD2WC&r^#a+X@c~SLYthy$HB=6U6H^izl UYa`cY-c7r)>jX*KZ*#H%0KBAcdH?_b diff --git a/docs/API/_build/doctrees/sites/api_ref/Rim_scheme.doctree b/docs/API/_build/doctrees/sites/api_ref/Rim_scheme.doctree index dda2bd447531347be1d4c7dcbb2e874a3a773a45..8a1d981efb3db778a341e6fab12eb249c626edcf 100644 GIT binary patch delta 160 zcmeA?#n^d@al=Zs$&VT3C(DPKZT`o0lLN{z->fUJoEgTMtSi8|*;wR|C`@3px7Hyh z7-zD#7T@MV-8Eb=fz8Dhii~j1D@$({7-zDN6VK-9cJXkNH~Tng2|{HZHeU_a2AjHB IKI|1A0HeM-Jpcdz delta 292 zcmeA?#n^d@al=YB{$#6|(&E&j;+X8*($tug$^2n%oBy%h~q zYs8}i5=0TrIrInEkHw+s4vrF~&wF3vIiGXR7rv}!Wi_i_km!$XjB2qd+)EKhPY}wy zc3SqxSa!E8|4o!VgSc+Y6P;4L*h??C@z=Kw>$>RzpLtai5T!x^qxY&3wh!#_*KkdZrrjK7&;7>Pf z{B=le+jx}VdgrrGka|{h2(4!&EbUg!sFEAuiv(Bs4?FMCCyRu)y|6gZHMaIGGEY>_ ooVYm>UnU(rdORQ&P%Ozp&|=Ar=4sx^+f>#hRLf=^vX<-r1ut#w>i_@% delta 1086 zcmZ{hKS%;$7{<|5PP;g+ArQnx3567lM35jqx|0wrtRaGfL-GcT{*VZf4F+w2C|?8x zO|C)F1L6?T5G~;lG+Q({#tn`Rnd`mI&-;6R@ArOY%Wk&p4@E)RPHN?naa7WF_sWJ= z;BqX5wh(m@0pq6E!E3UJwA8na8$eB-rfL~XuWBDdNx1YKlTK)X1ntrSGWx+DRjD8n z@=}z7>+lOjsEtD>9_L7Clm(+*3kNxNKv~w%oqxAX4^OLL5D!s=!Uu5=&Jrd~y(VW! z3X#P(nn*8=A?{N@B*AsngWqL8?a@`&1o3qW+AHMA6a+IZ(t}8L%w|`sPZk3CT+zBw zK9|Mp#wWEALuPZrQZqa~!|m3$lXwF|NB3DBLxOj70W9C7>lO;+I~Z<^F9+q}&o%)c zJ7IF|QMkTu6DhKB;l|wwd1&^Z`A?@vBPvyq40xp~p?7Y$DH|8-0-7~i2eDq5`~%cB BiSPgb diff --git a/docs/API/_build/doctrees/sites/api_ref/quantify_engine.doctree b/docs/API/_build/doctrees/sites/api_ref/quantify_engine.doctree index b3832f67081333b2b7b43f393c0f3ada2058f953..5928c5ab84021fcc42c6505858ec65a6dad38c25 100644 GIT binary patch delta 16245 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 diff --git a/docs/API/_build/doctrees/sites/api_ref/stack.doctree b/docs/API/_build/doctrees/sites/api_ref/stack.doctree index b3a02e1272b1ffa1cd89a5794879a47d30f935da..f4da888607025a66e6e6637d4654570de31acc85 100644 GIT binary patch delta 684 zcmX@}pYzOr&J8IXlm9WxPrtC1(P;Bl4mnl?Plb=48OAf7T=0fvbElvunuvzzMk!P^ z`Ud+@)lAkmVBPFt!mW;~Ze?^YJE}-eGN%)Y$Yg`nY@0tWeaV6%y4hg0zb&!|*XG+V zoDp_zE_fr&kC5kM;@te~w*ec9$o4`eMpGTQ(8T`&+d1?Z8xR(OL|z#)iU`5gY!7f_ z{QYzIcqGPubW+s|%dY!XK3opORvWc$`*jJ=F-vF$*yQ}C!Y-oD`? z<7{}`8czpu*|#sb&iI-Kq2u8bMni;>?J2Jqr>Y=iz4@8WGQqjifn1*LWkO7u+z7D` yicGxQ?c|sekbDUglSIVE_Gm+<-AHz1m@~yA*&!Lj^cSIbI*`l1y*z;_R2~2^N*oga delta 1076 zcmZ|NK`29U9Ki8@f5x8Sg+-)Etwd^1Bc)vS#xTM`4qhP?iDHIQa#OOyOuZW?2dyOk z*6%=$OWB1IS0y0_oJgAg=Jx(S*Z=2z`~UjAe=4n{(#r9qU7ct%!l9|fkTE+Ko-!t5 z_f8*A*j+Xf+{J1R0YM%q>5zcmJ3p(4uLInpol9Sfivux_X5(GWjykeB9Es$Uusxyp zD2QqCJpO+0oJ%3s;{EL;+&p!Y)A6O(3Y94QF_qc*=x9OwiGmFcDs70-GgSz-+_YQUounc}n;ugAewZVkj=(XoCN)~Su?__YRm z#Z+U?M?t}lEoh*YqN8BFnhG)J2tv|?*)c9oV6)8Hn1dHEjEN*fz)C2nNU@$~f(~>4 z7Ho)Duq(autfTO^LMg3%fgnCVK)qCT*_t~q@&lxW~I&l qP54pE$k(XjNE17i2Fz}7x|NMeLuRGYU_a7D88L;ZxH86uYJLHxahfIo diff --git a/docs/API/_build/doctrees/sites/lib_doc/dataprocessing/00_overview.doctree b/docs/API/_build/doctrees/sites/lib_doc/dataprocessing/00_overview.doctree index 881a4f76634b6d246c9fe62ce8e62c49fe7c518a..c58fef7f95545707f6c7c61639046c9717291a2c 100644 GIT binary patch delta 146 zcmca6c|~%A5RYxBSXyRIYJ6s1acWUXW`16LYF=VePHIZw&W=#-qQr7If0s97C~rz> zZb5u;YDr0EUV3ri?#Y2X()xS688Q@W3-@LSWQY`p3Kf?o6_;d|l!6V*OUz9zF5FjJ pxIaTGLoGw5w(!8@**xkZ2V)Blg|Zc;re&rS7ap!HJW`sZ2LMvhIsyOy delta 148 zcmca2c};SI5RZM~&QP(m%$(Ht%)H{%qLR$~y!h0-#H5_ml)_ydq1;7@<#7IPZ^lsG zl+xUS_~O)(lFYpH;=(eY!O1gu)I|=(79I{|D@skvOerorQd@YmG)WHt{c1Zl diff --git a/docs/API/_build/doctrees/sites/lib_doc/dataprocessing/03_inspection.doctree b/docs/API/_build/doctrees/sites/lib_doc/dataprocessing/03_inspection.doctree index 073448c39987865871b4580c4dc44196ff8c50d3..1ce54f7151ed1a836faebf927436c97cdd45f5dd 100644 GIT binary patch delta 154 zcmZ4bi)q0xrVR>voI}OZGILVnGxLg5i%K%{^WszU5|eULQ;IgQGB9+6au+3*!-X~i zg}fO!AqHRDjLn%Wq oL#4K8`{dhuv=nv#MPiF~0?AOeqSUm^l;WaYV5Pf(WNDHf0ASxW^8f$< delta 164 zcmZ4Ri)raErVR>vSc^8WGB8Y5*drOd5yO)1SS zh%ZhpDap)BFD}{)6!j}j3KcD`%uUMAiBHSVFUiXOpWGGuvYFcJWanWwD(mg=3G)WHt Dq+d7g diff --git a/docs/API/_build/doctrees/sites/release_notes/01_latest.doctree b/docs/API/_build/doctrees/sites/release_notes/01_latest.doctree index 0df59261e5d75892e9cb68e7b3023af4d5226fc9..31139a5ef0af4c9b7ab82e4ed54eb00326009db8 100644 GIT binary patch literal 29670 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�+KBb!4=Mly-iPp)FCW(L-2&c6x}r)~ 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

                          iGJ9hzFkh7-3Wcq`EZ>+C!R3uDJG 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

                          Ba;{=eq=I!qb#N%Xt=3( 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 diff --git a/docs/API/_build/html/_sources/sites/release_notes/01_latest.rst.txt b/docs/API/_build/html/_sources/sites/release_notes/01_latest.rst.txt index c614f5647..4eb81c383 100644 --- a/docs/API/_build/html/_sources/sites/release_notes/01_latest.rst.txt +++ b/docs/API/_build/html/_sources/sites/release_notes/01_latest.rst.txt @@ -2,9 +2,39 @@ :maxdepth: 5 :includehidden: -==================== -Upcoming (September) -==================== +=================== +Latest (01/10/2018) +=================== + +**New**: "rewrite" of Rules module (affecting sorting): + +**sorting "normal" columns**: + +* ``sort_on`` always '@' +* ``fix`` any categories +* ``sort_by_weight`` default is unweighted (None), but each weight (included +in data) can be used + +If sort_by_weight and the view-weight differ, a warning is shown. + +**sorting "expanded net" columns**: + +* ``sort_on`` always '@' +* ``fix`` any categories +* sorting ``within`` or ``between`` net groups is available +* ``sort_by_weight``: as default the weight of the first found +expanded-net-view is taken. Only weights of aggregated net-views are possible + +**sorting "array summaries"**: + +* ``sort_on`` can be any desc ('median', 'stddev', 'sem', 'max', 'min', +'mean', 'upper_q', 'lower_q') or nets ('net_1', 'net_2', .... enumerated +by the net_def) +* ``sort_by_weight``: as default the weight of the first found desc/net-view +is taken. Only weights of aggregated desc/net-views are possible +* ``sort_on`` can also be any category, here each weight can be used to sort_on + +"""" **New**: ``DataSet.min_value_count()`` @@ -102,85 +132,3 @@ The method automatically checks if the included variables are arrays and adds them to ``Batch.summaries`` if they are included yet. """" - - -=================== -Latest (04/06/2018) -=================== - -**New**: Additional variable (names) "getter"-like and resolver methods - -* ``DataSet.created()`` -* ``DataSet.find(str_tags=None, suffixed=False)`` -* ``DataSet.names()`` -* ``DataSet.resolve_name()`` - -A bunch of new methods enhancing the options of finding and testing for variable -names have been added. ``created()`` will list all variables that have been added -to a dataset using core functions, i.e. ``add_meta()`` and ``derive()``, resp. -all helper methods that use them internally (as ``band()`` or ``categorize()`` do -for instance). - -The ``find()`` method is returning all variable names that contain any of the -provided substrings in ``str_tags``. To only consider names that end with these -strings, set ``suffixed=True``. If no ``str_tags`` are passed, the method will -use a default list of tags including ``['_rc', '_net', ' (categories', ' (NET', '_rec']``. - -Sometimes a dataset might contain "semi-duplicated" names, variables that differ -in respect to case sensitivity but have otherwise identical names. Calling -``names()`` will report such cases in a ``pd.DataFrame`` that lists all name -variants under the respective ``str.lower()`` version. If no semi-duplicates -are found, ``names()`` will simply return ``DataSet.variables()``. - -Lastly, ``resolve_name()`` can be used to return the "proper", existing representation(s) of a given variable name's spelling. - -"""" - -**New**: ``Batch.remove()`` - -Not needed batches can be removed from ``meta``, so they are not aggregated -anymore. - -"""" - -**New**: ``Batch.rename(new_name)`` - -Sometimes standard batches have long/ complex names. They can now be changed -into a custom name. Please take into account, that for most hubs the name of -omnibus batches should look like 'client ~ topic'. - -"""" - -**Update**: Handling verbatims in ``qp.Batch`` - -Instead of holding the well prepared open-end dataframe in ``batch.verbatims``, -the attribute is now filled by ``batch.add_open_ends()`` with instructions to -create the open-end dataframe. It is easier to to modify/ overwrite existing -verbatims. Therefore also a new parameter is included ``overwrite=True``. - -"""" - -**Update**: ``Batch.copy(..., b_filter=None, as_addition=False)`` - -It is now possible to define an additional filter for a copied batch and also -to set it as addition to the master batch. - -"""" - -**Update**: Regrouping the variable list using ``DataSet.order(..., regroup=True)`` - -A new parameter called ``regroup`` will instruct reordering all newly created -variables into their logical position of the dataset's main variable order, i.e. -attempting to place *derived* variables after the *originating* ones. - -"""" - -**Bugfix**: ``add_meta()`` and duplicated categorical ``values`` codes - -Providing duplicated numerical codes while attempting to create new metadata -using ``add_meta()`` will now correctly raise a ``ValueError`` to prevent -corrupting the ``DataSet``. - ->>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')] ->>> dataset.add_meta('test_var', 'single', 'test label', cats) -ValueError: Cannot resolve category definition due to code duplicates: [2, 3] diff --git a/docs/API/_build/html/_sources/sites/release_notes/02_archive.rst.txt b/docs/API/_build/html/_sources/sites/release_notes/02_archive.rst.txt index 179034afd..d2099d199 100644 --- a/docs/API/_build/html/_sources/sites/release_notes/02_archive.rst.txt +++ b/docs/API/_build/html/_sources/sites/release_notes/02_archive.rst.txt @@ -8,6 +8,91 @@ Archived release notes ====================== +--------------- +sd (04/06/2018) +--------------- + + + +**New**: Additional variable (names) "getter"-like and resolver methods + +* ``DataSet.created()`` +* ``DataSet.find(str_tags=None, suffixed=False)`` +* ``DataSet.names()`` +* ``DataSet.resolve_name()`` + +A bunch of new methods enhancing the options of finding and testing for variable +names have been added. ``created()`` will list all variables that have been added +to a dataset using core functions, i.e. ``add_meta()`` and ``derive()``, resp. +all helper methods that use them internally (as ``band()`` or ``categorize()`` do +for instance). + +The ``find()`` method is returning all variable names that contain any of the +provided substrings in ``str_tags``. To only consider names that end with these +strings, set ``suffixed=True``. If no ``str_tags`` are passed, the method will +use a default list of tags including ``['_rc', '_net', ' (categories', ' (NET', '_rec']``. + +Sometimes a dataset might contain "semi-duplicated" names, variables that differ +in respect to case sensitivity but have otherwise identical names. Calling +``names()`` will report such cases in a ``pd.DataFrame`` that lists all name +variants under the respective ``str.lower()`` version. If no semi-duplicates +are found, ``names()`` will simply return ``DataSet.variables()``. + +Lastly, ``resolve_name()`` can be used to return the "proper", existing representation(s) of a given variable name's spelling. + +"""" + +**New**: ``Batch.remove()`` + +Not needed batches can be removed from ``meta``, so they are not aggregated +anymore. + +"""" + +**New**: ``Batch.rename(new_name)`` + +Sometimes standard batches have long/ complex names. They can now be changed +into a custom name. Please take into account, that for most hubs the name of +omnibus batches should look like 'client ~ topic'. + +"""" + +**Update**: Handling verbatims in ``qp.Batch`` + +Instead of holding the well prepared open-end dataframe in ``batch.verbatims``, +the attribute is now filled by ``batch.add_open_ends()`` with instructions to +create the open-end dataframe. It is easier to to modify/ overwrite existing +verbatims. Therefore also a new parameter is included ``overwrite=True``. + +"""" + +**Update**: ``Batch.copy(..., b_filter=None, as_addition=False)`` + +It is now possible to define an additional filter for a copied batch and also +to set it as addition to the master batch. + +"""" + +**Update**: Regrouping the variable list using ``DataSet.order(..., regroup=True)`` + +A new parameter called ``regroup`` will instruct reordering all newly created +variables into their logical position of the dataset's main variable order, i.e. +attempting to place *derived* variables after the *originating* ones. + +"""" + +**Bugfix**: ``add_meta()`` and duplicated categorical ``values`` codes + +Providing duplicated numerical codes while attempting to create new metadata +using ``add_meta()`` will now correctly raise a ``ValueError`` to prevent +corrupting the ``DataSet``. + +>>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')] +>>> dataset.add_meta('test_var', 'single', 'test label', cats) +ValueError: Cannot resolve category definition due to code duplicates: [2, 3] + + + --------------- sd (04/04/2018) --------------- diff --git a/docs/API/_build/html/genindex.html b/docs/API/_build/html/genindex.html index b5132efe3..4a59564b2 100644 --- a/docs/API/_build/html/genindex.html +++ b/docs/API/_build/html/genindex.html @@ -90,9 +90,9 @@

                          Quick search

                          • Release notes
                              -
                            • Upcoming (September)
                            • -
                            • Latest (04/06/2018)
                            • +
                            • Latest (01/10/2018)
                            • Archived release notes
                                +
                              • sd (04/06/2018)
                              • sd (04/04/2018)
                              • sd (27/02/2018)
                              • sd (12/01/2018)
                              • diff --git a/docs/API/_build/html/index.html b/docs/API/_build/html/index.html index 6c21b7048..31ebf4ba6 100644 --- a/docs/API/_build/html/index.html +++ b/docs/API/_build/html/index.html @@ -90,9 +90,9 @@

                                Quick search

                                • Release notes
                                    -
                                  • Upcoming (September)
                                  • -
                                  • Latest (04/06/2018)
                                  • +
                                  • Latest (01/10/2018)
                                  • Archived release notes
                                      +
                                    • sd (04/06/2018)
                                    • sd (04/04/2018)
                                    • sd (27/02/2018)
                                    • sd (12/01/2018)
                                    • diff --git a/docs/API/_build/html/objects.inv b/docs/API/_build/html/objects.inv index 4b6e8180792853d538e1be1f3a33fe3128febec9..09c47acdbb93c670f59f97e11460720c5cc5f159 100644 GIT binary patch delta 2316 zcmV+n3G?>-67Uj`eSb@j<2Dk2@BS46b`k6z`r)402j;e$1W14cJG&F?m7t}v%``=7 zNXp%sU!Ni=`Oz(jvWqucW_?AGRJ};~`#l%xek&v>UPIAzpSW{IY+47!y0F55ec+8? z2DT61hqilH%Rl|OxLbW~xpJcE?!$F2T1sy1bz=9WRwZ{{x_<_@*VWbHO?R_!1-8U{ zg_{9V^U4cb$DJr|LRfydNxy8Z12Z$~vB9+HJ+CTO4pTR!K1tbd^&Byl#920!NlXo9 z2c(XqCS(%u19$ujxaleB8%&FaDM7t_M*jkclr(wOk>qhjR5U=xT-N}Jfi?wp+K6vN zj0A4jmX}T&OMfJ3MeK{NfR(ycj*i(HT{FoyAjvFCCorK{#fi(l)OEuRb%@mg40U{) zjtpSj7~W+V6a)AmFbl+cGTNeH-q0YeV<=KH2d|E{YRKbKfe{CwA~gCcDkK?!EP#k| zubr-`#;y`IC@Zwe2oh??YTh(4RJomKbVCNPs%@n3o_~S3`to3)Zd^wvglCG_lUE0- z$_F}d*%AjQhs(vR#u3-uRuE$u4>+y645n`@F+l@GV`x)m>5GpdHr$naI>x(ijF420 zF2QR_3~T}hJzn`t!?aR%Y%i(`RCMXIyW4@wFy5ZhpgqJ$hh~59#(?EALI^`=K|`dn zzcfN9G=CXV6X#u6CdD%_)nboCNo6Tcd)`CTr8J#EHyQMGB88$&PFN%vZiSqvdjNBq z{cbgFerwF1g3ry69{MsMZCtdeh+fy{aa2dp=%$s_@Eo(Jit!8~dH@~6VoPTYSLr0% zSZaTK5>G_b`T)!~$ibl-nI4oOkTu7`IuQ|THh&p%DFfjN%`!@nlx3J2E=&eQFzgH} z;m945JWG^pz-oz$DZ@Ijm=i&+B8mZWt;=XBSxd{z6ZQ550FY3+IPcFujIb@n#-Pxk z^|5HEB5z1I!&fm~^Fgk{N-BkJZv`M>>#{|k7r)5k+D znx!!JrIsi>yZ){NRo@p*ym2@6g&@!<8C+cbIfIl)@?xCKL45#=~D?JGb0EVf^4rYyf z5@ev!vt^znAtdPZjx310W*J|G?bz{Y2|rJXg*i!_8cG8aTTf|#njgY6z-Oa0LMmuT z5OzBQyZK2>5Sm7IA-hf%Y3D&@TYviv9tu?W2q>&=>9r@?k@VWCDizJ86y+ zuM2Jq(h2j3Gzpmb33C3uB_`H!`l!F z2*OQC5jMIZcHTdy1nF5%pAe3T0aP~<5@vr^G;X-vD6JA9+pMfVqb_F87tM*s{PXs% z+XhN}>=fFe+dC2G?Zy%K^?z6apEjYf%`9MeHOp(C@5d5Y&7XbwxNZ_Bl$P?6yG&!C z9VnqHcrDPre~Rw{-K~q^YG_yd>_4tf%_?2`MrSRCYTKvcbBC%`VOw*6t_YEq(^@_Y zx2pYq`a4xkcQ^bHS-+@8b zNc-34p9{PW22=@lHS!YXH0$(^W)VKFM&B5mkw1H-u;{<6MvKeSjPvSq(SKRZ)J3OB z@5`sf#ii9$%07*JlhzmbeKiuVP6JNiY!N=L-mmUy;iGXV{HN8_gE0>{dw7b|_tk7T zH%%Oi`-eZ1JSrGMBkWITjp-?(hq^xB?m8u|bBm&d7V*1sqgQpXhdqIJJ&{RMOr z55+Hd&HD>pd~NQ7O0*W}^zpG8*Zss!TUaU0XKnk(whGEl1gDp4{xFK1>r0`-dtD{pE4}uzq}adR+hWujBkD z=93v;Sbs_>IJWH9ar{yhCVdkh*j~T*7mlnF7`=|UxRgXx`jdXSbNX($II8W9;6ooW z4*$zhavAIBi7qU}^d3rVLk8!H7yqUuMr36lmlkINMLc!2sMp7r{IBfhAlLURew$jm m?}U#Bph94AweCytOXvN=>2Ou}!we@jLZazd{RIIGWWBHWG delta 2329 zcmV+!3Fh|j68#d8eScq%j(G{>#x608mJELnR`4%LZW$6Sa6stJV>`PrY+)#&D9l=n? zx9P|L#*N`!hCwla4+675yeFeAI_516(mIABHFNOdXjVfWmkNwH0u`Zgt)fDb5y%RN zC=c4{n(FK-QG>EVtBfF__N?Yj6GN5TiAFbM0IS+Y3V**c5Uno{2I|IjbV7Khh&_36 zpsIYJBbP04aB?iUnAJF<-R%T1mhphm?lQQ(ox}tU6pf)xnWZm2ir8{j9_Sdqd}D;9 za&!q^N@8FWFu3BCPc%#`WycPpszAjhopyJ7a2dwiQyTP#80pab58fECTuumKxLD8- zsq8O}5Pu3?hSbD)7nVu!1WdKqBT-UWiqoF=5Opa{XV6^+-A<%Xw8;rZlHpd!iMj_c zr`aD?)8@Cv{3)!t8PY>v2BeLPHWhKz^?4k1A!u~dN@{qH*;B=Mh7dh~PGPa7vxcj5 zl5H%tKR$^kB5HjE<{RYT(49;V$`Ht!Q(>Kmh<`Pk47rqn@PuX=rAW#$Obr(%10pzf z291W zIMX<)Y6V!mWd zZGZRKa6;`gnI}a_&AqHPj?QRJNnA*5BL#7?95?Q?4Y#--xdPU1sl|84pJ)z}Y`&PI zgyIDiQh^jTidEzd31_&n4cb46(yA@BzSk;p@Y||vh4%|orqX+>#IDN`;32hq|hs8&mi^(+J961Y)~LqnjB$S^5lrRIZ^&^|KWE5&vyQJ z$VIah=DyStg%|DbI$#Am-b$B|=j$eo6GzC`$(?~OEym{^nHYoC>6wU4(d&WQ3k3^| z`;yD0^6oXHMCtAd6ugwOU0ju}fF*a)u3+trUd$G+K!r#m4@zlMS-R zD1YjJACq;5Wz4Fh)XRvtrxUjv&}cDp-;s$`Ti!m(*aFwW13^A)=#X5%P=6=QaiZM4 zEId{uy9RD~bbiBIpv1jiKAayR1~?-}>@$J9X<8>rI?l9;4hiLPZusIBTYsA6lbHDl z^S{mrg+cKiHr$!m>nUosbSGmL58HBO&4T7e(WS+l|sH5wb1H`ZMZc@qE#ec+5X< z@49WE#K%sdAG*C0ao%nmfq!3574Uf%8rv)ahF7z^_T_%8fz|xUmyhcvaYAV=FS*M! z1=@oWs)CmS{rkuGF3{a$FN`~K5i!87+jD)d!?}GzicLp%kzx$>T}V5*(}sW z=SgqNr^Usk&0NYpk9?KZ7x;ZM5wFez#&EU>A2)BUdtUhD918zwGxuOD1I`|vV*I{Y z4Cm&FQ*mE0T)dALdw;eRTZW8>aO@k+md)GtY}3g9-d~=kKC=Erv5-2Zz!$B1tMwOf znRqCE!E1iK;Ki5bKBz=*fr~yqR@1uQv2hD4rTMIF|JYVR*+_7Dz2^7B`4zE3^?BpL z0inko0eW|)d0OEIj9<2K8A_=en+}c`!)L#B?o|xlAUb>dT7Ow9WMx;H%A2;mt%U0{ z;K$*8?<96;*&B(jw?g^(!my&1?ls>p891Qme|@mD?gr*%Mpsyj4H6n3&tF4Xt@B*! z!T}fK^@QSgOz8Cree2#V9{!Ueb5?VOi)4-6xI)Fb*yT-)*AuGWGn{%107K}vTJyQo zVEgg=`GcNN{eK~(zFuOr-jhgF`cp6Oqki8sq3|Cp97)+}Q(vX^QZB0g?VPmsj{k|? zcrVH;%yy87N`LX63;*iAk0iZULl19KTED}b*g9RaP%yme&fkdkP9j{4;p6bjt4_rA zah-p7+qPdg6qlV9w)mzPYT9UTGm0KV<=@5^!faaEWqwK7|H){a8kYb z{OvO|n14HN!Tfz%!8_(NBVVOTX*jj)ABy;;Dopw=-m`;#_HQp)B`_48S}#))QR%<+ z^PSUo!^KH$uLK|Z_;dJ^kCMw+luvYFC8oC%u?y*-E1vypn;71eeUMw62^8@N*Q#Eh z{xRUnZVqyNzv8z!<9#E1I!zU#l&f`LieE+oKN5~7wZgw|II$HH{m|x5j>g=xEfSEP diff --git a/docs/API/_build/html/search.html b/docs/API/_build/html/search.html index e442bf83f..c4cf1e99c 100644 --- a/docs/API/_build/html/search.html +++ b/docs/API/_build/html/search.html @@ -86,9 +86,9 @@
                                      • Release notes
                                          -
                                        • Upcoming (September)
                                        • -
                                        • Latest (04/06/2018)
                                        • +
                                        • Latest (01/10/2018)
                                        • Archived release notes
                                            +
                                          • sd (04/06/2018)
                                          • sd (04/04/2018)
                                          • sd (27/02/2018)
                                          • sd (12/01/2018)
                                          • diff --git a/docs/API/_build/html/searchindex.js b/docs/API/_build/html/searchindex.js index ee2a67ab7..baec5bb34 100644 --- a/docs/API/_build/html/searchindex.js +++ b/docs/API/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/Stack","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/lib_doc/overview","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:52,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\Stack.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\lib_doc\\overview.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],drop_duplicates:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],min_value_count:[4,2,1,""],names:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],resolve_name:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],unbind:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[10,2,1,""],count:[10,2,1,""],exclude:[10,2,1,""],filter:[10,2,1,""],group:[10,2,1,""],limit:[10,2,1,""],normalize:[10,2,1,""],rebase:[10,2,1,""],rescale:[10,2,1,""],summarize:[10,2,1,""],swap:[10,2,1,""],unweight:[10,2,1,""],weight:[10,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[7,2,1,""],add_link:[7,2,1,""],add_nets:[7,2,1,""],add_stats:[7,2,1,""],add_tests:[7,2,1,""],aggregate:[7,2,1,""],apply_meta_edits:[7,2,1,""],cumulative_sum:[7,2,1,""],describe:[7,2,1,""],freeze_master_meta:[7,2,1,""],from_sav:[7,3,1,""],load:[7,3,1,""],recode_from_net_def:[7,3,1,""],reduce:[7,2,1,""],refresh:[7,2,1,""],remove_data:[7,2,1,""],restore_meta:[7,2,1,""],save:[7,2,1,""],variable_types:[7,2,1,""]},"quantipy.Test":{get_se:[10,2,1,""],get_sig:[10,2,1,""],get_statistic:[10,2,1,""],run:[10,2,1,""],set_params:[10,2,1,""]},"quantipy.View":{get_edit_params:[8,2,1,""],get_std_params:[8,2,1,""],has_other_source:[8,2,1,""],is_base:[8,2,1,""],is_counts:[8,2,1,""],is_cumulative:[8,2,1,""],is_meanstest:[8,2,1,""],is_net:[8,2,1,""],is_pct:[8,2,1,""],is_propstest:[8,2,1,""],is_stat:[8,2,1,""],is_sum:[8,2,1,""],is_weighted:[8,2,1,""],meta:[8,2,1,""],missing:[8,2,1,""],nests:[8,2,1,""],notation:[8,2,1,""],rescaling:[8,2,1,""],spec_condition:[8,2,1,""],weights:[8,2,1,""]},"quantipy.ViewMapper":{add_method:[9,2,1,""],make_template:[9,2,1,""],subset:[9,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[10,1,1,""],Rim:[6,1,1,""],Stack:[7,1,1,""],Test:[10,1,1,""],View:[8,1,1,""],ViewMapper:[9,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":39,"boolean":[4,6,7,18,35],"case":[0,4,5,6,7,10,12,13,15,18,20,23,24,26,28,34,35,37,38],"class":[2,3,4,5,6,7,8,9,10,11,22,34,35],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,34,35,38,39],"export":[0,3,4,20,35],"final":26,"float":[4,5,7,10,19,20,21,22,23,24,31,34,35],"function":[4,7,20,25,26,28,31,34,35],"import":[4,10,11,15,20,22,25,26],"int":[4,5,7,10,19,20,21,22,23,24,25,34,35],"long":[25,34,35],"m\u00fcller":0,"new":[3,4,5,7,9,10,12,21,23,24,26,31,34,35,38,39],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,34,35],"short":[9,19,21,24,35],"sigur\u00f0sson":0,"static":[2,3,4,7],"switch":[24,34,38],"true":[4,5,6,7,9,10,13,20,21,22,24,25,26,31,34,35,38,39],"try":[4,7,23,35],"var":[4,10,15,34,35,38],"while":[4,15,19,23,24,34,35],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31,34],NPS:31,Not:[19,24,31,34],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,34,35,36,37,38],Their:5,Then:[21,35],There:[13,19,21,22,31,35],These:[15,28,31,35],Use:[4,10,24,35],Uses:4,Using:[7,18,21,23],Will:[4,5,7,10,35],With:[13,22,23,24,26,31],Yes:[20,35],__init__:[12,35],__setitem__:35,_band:4,_batch:[7,31],_cumsum:[31,35],_data:[4,11,21],_dimensions_suffix:35,_get_chain:35,_grid:[35,38],_intersect:[28,31],_meta:[4,11,12,13,22,28,39],_missingfi:10,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:35,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,35],about:[13,19,22,36],abov:[24,31,39],absorb:35,accept:[23,26,35],access:[4,7,19,20,22,35,36],accessor:22,accid:[21,35],accommod:14,accompani:[19,35],accord:[4,7,10,26,31],accordingli:26,account:[4,5,34,35],achiev:21,across:[10,22,35],act:[5,24,28],activ:[19,20,24,35],add:[3,4,5,7,9,13,14,23,24,28,31,34,35,37],add_batch:[12,31],add_chain:3,add_data:7,add_filt:[14,35],add_group:6,add_i:13,add_link:[7,28,31],add_meta:[4,23,34,35,37,39],add_method:9,add_net:[7,31,35],add_open_end:[13,34,35],add_stat:[7,31,35],add_test:[7,31,35],add_tot:35,add_x:[13,31],add_y_on_i:[13,28,31,35],adddit:20,added:[4,7,12,13,14,23,24,28,31,34,35,38],adding:[4,14,18,20,31,35,39],addit:[4,13,19,22,31,34,35],addition:[19,34,35],adjust:[34,35],adopt:13,aerob:24,affix:7,after:[4,7,10,21,26,28,31,34,35],afternoon:22,again:[22,23,31],against:[4,5,10,13,35],age:[4,13,14,22,24,26,28,31,35],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,35],agg:[8,13,31],aggnam:8,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,34,35],aim:[0,35],alasdair:0,alert:34,alexand:0,algorithm:[4,5,10,35],alia:[4,21],align:35,all:[2,3,4,5,7,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,34,35,37,38],allign:35,allow:[4,14,15,23,24,31,34,35],alon:[22,35],along:[2,10],alongsid:[7,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,35],also:[4,7,10,11,12,13,14,22,23,24,26,28,31,34,35,37,38],altern:[4,23,31,37],although:22,alwai:[4,15,23,24,26,31,35,39],amount:[4,14,21,28,31],analysi:[0,35],ani:[4,5,7,8,10,13,19,21,22,23,25,26,34,35],anim:[4,19],anoth:[7,13,23,24,26,35],answer:[4,7,10,19,22,23,24,35],anymor:[34,35],anyth:26,anywher:7,api:4,appear:[4,35,37],append:[4,5,18,31,38],appli:[4,5,7,9,10,15,20,21,22,24,28,31,35,39],applic:[4,24,26],apply_edit:4,apply_meta_edit:7,apporach:21,approach:37,appropri:[4,5],arab:35,arbitrari:7,arbitrarili:[4,21,25,26],archiv:33,argument:[5,9,11,20,23,24,35],aris:28,arithmet:10,around:[4,35],arrai:[4,7,10,11,18,21,22,23,31,34,35,36,37],array_item:35,array_var:39,array_var_1:39,array_var_2:39,array_var_3:39,array_var_grid:39,arriv:10,as_addit:[13,34,35],as_delimited_set:35,as_df:10,as_float:35,as_int:35,as_singl:35,as_str:35,as_typ:[4,35],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,10],aspect:35,assess:[0,38],assign:[4,6,35,37],assignd:24,associ:[2,7],assum:[4,10,28,38],attach:[4,7,21,23,35],attempt:[4,34],attribut:[2,4,11,22,28,31,34,35],audit:35,auto:[4,7,34],autom:[0,7],automat:[4,13,14,20,22,23,24,31,34,35,39],auxiliari:10,avail:4,avoid:26,axes:4,axi:[2,3,4,5,7,8,10,15,19,22,35],axis_edit:[4,35],b_d:4,b_filter:34,b_name:31,back:[4,5,38],badli:[4,35],band:[4,7,18,34,35],band_numer:35,bank:3,bank_chain:3,bar:35,base:[4,5,7,8,9,10,18,20,22,31,35],base_al:10,base_text:15,based_on:[4,35],baselin:9,basi:26,basic:[20,22,25,27,35],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,7,15,28,31,34,35],batch_nam:[4,7],batchnam:[11,13],bchain:3,becaus:[24,26,35],becom:[4,10,26,35],been:[4,7,10,13,21,24,26,34,35,39],beer:38,befor:[4,7,10,13,14,24,26,31,35],begin:[4,26],behaviour:[5,8,22,26,35,39],being:[4,24,39],belong:[4,11,12,13,31,34,35,38],below:[4,10,13,26,35],benefici:21,benefit:26,better:28,between:[4,10,13,31,35],bia:10,big:21,binari:35,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,blacklist:[4,35],blank:37,blueprint:35,board:[19,24,39],bool:[4,5,7,9,10,22,38],border:4,both:[4,7,12,13,19,20,21,23,24,26,28,34,35,39],bottom3:7,bottom:35,bound:35,bracket:[7,35],brand:38,break_bi:[13,34],breakfast:22,brief:35,broader:19,buchhamm:0,bug:35,bugfix:[34,35],build:[0,3,4,5,10,15,18,19,35],built:[14,31],bunch:34,by_nam:[4,35],by_typ:[4,21,22,35],bytestr:2,cach:[4,7],calc:[5,7,8,10,31,35],calc_onli:[5,31],calcul:[4,5,7,10,14,27,34,35],call:[4,7,21,22,24,28,34,35,39],came:35,can:[2,4,5,7,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,34,35,37,38,39],cannot:[26,34,35],cap:6,carefulli:4,carri:5,case1:38,case2:38,case3:38,casedata:[4,20],cast:35,cat:[4,19,23,34,37,39],cat_nam:[7,35],categor:[4,5,7,18,22,23,27,34,35,36],categori:[4,5,7,19,22,23,34,35,37],categorized_nam:[4,35],caught:35,caus:[34,35],caution:4,cave:[19,24,39],cbase:[7,31,35],cbase_gross:35,cell:[5,10,11,26,35],cell_item:14,cellitem:35,central:5,certain:[4,22,35],chain:[0,1,3,16,22,35],chainmanag:34,chainnam:2,chang:[4,6,7,10,18,21,24,34,35],charact:[4,23,25],characterist:[19,24],chart:[0,35],check:[4,5,7,10,22,26,34,35],check_dup:35,checking_clust:7,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,7,21,24,35],clean_text:[23,35],clean_up:[4,35],clear:35,clear_factor:4,client:34,clone:[4,18],close:12,cluster:[0,1,2,7,13,14,15,35],code:[4,5,6,7,8,10,18,19,22,23,24,31,34,35,37,39],code_count:[4,22,25,35],code_from_label:[4,35],code_map:4,codes_from_nam:[4,35],codes_in_data:[4,22,35],cola:38,collect:[4,5,7,11,13,14,19,21,22,24,27,31,35],collect_cod:[7,35],colour:35,coltest:[5,7,9],column:[2,4,5,6,7,8,10,18,20,22,23,24,26,31,34,35,38,39],combin:[4,5,7,16,22,25,26,28,31,35],combind:38,come:35,comma:25,common:[8,19,24,35],commun:7,compabl:4,compar:[4,5,10,34,35],comparison:10,compat:[4,7,20,35,36],compatibilti:39,complet:[4,10,35],complex:[4,5,6,18,19,21,22,26,31,34,35],compli:[4,26],complic:18,compon:[4,5,7,8,11,15,18,21,22,23,24,35],compos:7,compound:3,comprehens:26,compress:7,comput:[0,4,5,7,9,10,27,28,31,35],concat:[2,22,35],concaten:[2,26],concern:31,cond_map:4,condit:[4,8,10,18,21,22,25,34,35],confirm:7,conflict:35,conjunct:35,connect:[4,7,12,13,35,39],consequ:[28,37],consid:[4,5,7,10,24,34,35],consist:[4,10,11,19,21,23,24,35],constitut:13,constrcut:7,construct:[4,11,13,14,19,21,23,28,31,35,39],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,35,38],content:[21,28],context:[19,23],contrast:[19,24],contributor:0,control:[4,5,7,8,10,23,24,26,35],convcrit:6,convent:[4,10,35,39],convers:[4,18,34,35],convert:[0,4,10,20,24,34,35,39],coordin:9,cope:28,copi:[2,4,9,12,18,21,23,26,34,35,39],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,35],copy_not:[4,35],copy_of_batch1:12,copy_onli:[4,35],copy_to:[4,23,35],core:[5,20,22,25,34,35,37],correct:[10,23,35],correctli:[34,35],correspond:[4,5,10,22,23,24,35,37,39],correspons:35,corrupt:34,could:[23,24,26,35],count:[4,5,7,8,10,31,34,35,38],count_not:[4,22,35],count_onli:[4,22,35],counterpart:35,counts_cumsum:[31,35],counts_sum:31,cpickl:2,crash:35,creat:[2,3,4,6,7,10,11,13,14,15,16,18,20,22,24,28,31,34,35,36],create_set:[4,35],creation:[10,18,23,35],cross:[13,28,31,35],crossbreak:28,crosstab:[4,24],crunch:35,csv:[0,4,7,11,18],cum_sum:10,cumul:[7,8,10,27,35],cumulative_sum:[7,31],current:[0,2,4,7,24,35],custom:[4,7,16,18,20,24,34,35],custom_text:[7,31],customis:35,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:10,cycl:[4,9],dai:[22,26],danish:19,data:[2,4,5,6,7,8,10,11,14,15,20,21,23,24,28,31,34,35,36,38],data_df:[4,35],data_kei:[4,7,14,28,35],datafil:[4,35],datafram:[3,4,6,7,8,10,18,19,21,22,23,28,31,34,35],datakei:35,dataset:[0,1,7,11,12,13,18,20,22,24,28,31,34,35,36,37],dataset_left:4,dataset_right:4,datasmoothi:0,datast:34,date:[4,7,19,20,21,22,23,24,35],dates_as_str:20,ddf:[0,4,35],deafult:[5,10],deal:20,decim:4,deciph:[0,18],deck:0,decod:[7,34],decode_str:7,decor:35,deep:4,deepcopi:35,defin:[2,3,4,7,10,13,14,19,21,23,28,31,34,35,37],definit:[2,4,5,7,10,14,19,24,26,27,28,34,35],definiton:[9,19,35],defint:[0,2,4,5,6,7,10,14,19,35],defintit:[21,39],defintiton:23,del:35,deleg:26,delet:[4,7,35],delimied_set:22,delimit:[4,7,19,20,21,22,23,24,25,26,34,35,37],delimited_set:[22,24,35],demograph:21,depend:[5,13,14,34,35],deprec:[7,35],deriv:[4,10,18,21,34,35,37],derivc:9,derive_categor:35,derot:[4,35,36],desc:4,descend:4,describ:[0,2,3,4,7,14,19,21,28,31,35],descript:[4,5,7,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,35,37],detail:[8,18,19],detect:[4,6,34],determin:[4,5,7],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,35],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,35,38],dictat:21,dictionari:[4,6,7,8,9,35],differ:[4,5,7,8,10,13,19,20,23,24,28,31,34,35,36,38],digest:19,dim:[5,10,31],dim_comp:35,dimens:[0,4,5,10,18,35,36],dimension:[4,35],dimensionizing_mapp:4,dimensions_comp:[4,20,35,39],dimlabel:35,dinner:22,direct:26,directli:[2,4,22,35],discret:[7,13],disk:[4,21,35],dispers:5,distinct:[19,31],distribut:[5,10],div:[7,35],dive:[19,24,39],divid:10,dk_filter:4,dms:4,dmsrun:4,doc:[22,35],docstr:6,document:[0,4,7,19,20,23,35],doe:[4,11,23,26,35],doesn:[4,25,26],dog:19,don:[19,22,24,37],done:[4,13,14],doubl:22,down:39,downbreak:[28,31,35],download:39,draft:35,draw:[21,24],drawn:[4,35],drink:38,drink_1:38,drink_2:38,drink_level:38,driven:0,drop:[4,5,7,10,21,35],drop_cod:[7,35],drop_delimit:4,drop_dupl:4,drope:[4,38],dropna:[4,6,35,38],dropx:4,dto:39,dtype:[20,22,26,35],due:34,dump:20,dupe:34,duplic:[4,23,34,35],durat:22,dure:[4,7,10,24],each:[2,4,5,6,7,13,14,19,23,24,28,31,35,38,39],eagleston:0,eas:35,easi:[0,19,22,23,35],easier:[12,22,28,34],easiest:22,easili:[4,12,20,31,35],ebas:[7,35],echo:4,ect:35,edit:[0,4,8,10,13,14,15,18,19,21,24,26,35],edit_param:8,eff:10,effect:[6,10,26,35],effici:35,ein:[19,23],either:[4,5,10,13,19,21,22,23,26,35],element:[4,7,10,19,35,39],eleph:4,els:26,emploi:19,empti:[4,7,22,24,26,34,35],empty_item:[4,35],en_u:7,enabl:[23,35],encod:[4,34,35],encount:20,end:[2,4,7,11,12,34,35],end_tim:22,enforc:35,eng:35,engin:[1,27,34],english:[19,35],enhanc:34,enough:4,enrich:35,ensur:[4,11,35],enter:[4,14],entir:[4,10,20,35],entri:[4,6,7,10,19,26,35],enumer:[4,7,23,24,26,37],environ:4,eponym:35,eqaul:4,equal:[4,10,35],equip:31,equival:[4,39],eras:[4,7],error:[4,10,34,35],escap:4,especi:[22,26,31],estim:10,etc:[4,19,21,28,31,35],ethnic:[13,22,28,31],even:[4,31,35],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,35],exactli:[4,24,25],exampl:[2,4,5,7,11,18,19,20,22,24,25,28,31,35,38,39],excel:[0,3,4,5,35],except:[4,21,35],exchang:[10,23],exclud:[4,5,7,8,10,31,35],exclus:[4,25,35],execut:4,exercis:[22,24],exist:[4,7,10,12,13,18,21,23,24,26,31,34,35],expand:[4,7,8,10,31],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,7,35,37],explor:19,expos:23,express:[4,5,6,7,10,22,25,35],ext_item:[4,35],ext_valu:4,ext_xk:35,ext_yk:35,extend:[4,5,7,13,18,22,31,35],extend_cod:[7,35],extend_filt:14,extend_i:[13,35],extend_item:[4,35],extend_valid_tk:35,extend_valu:[4,23,35,37],extend_x:[34,35],extens:[3,4,7,19,20,35],extra:35,extract:14,extrapol:25,factor:[4,6,7,35],factor_label:[7,35],factormap:4,fail:[4,35],failur:20,fall:[4,5,10],fallback:8,fals:[4,5,7,10,13,20,22,23,24,26,31,34,35],fast_stack_filt:35,favour:10,featur:[19,35],feed:[4,35],feedback:4,femal:[4,22],few:[11,21,22,35,38],figur:10,file:[2,3,4,5,7,11,20,21,22,28,35,39],file_nam:20,filenam:[2,7],fill:[4,18,34,35],fillna:[4,18],filter1:35,filter2:35,filter:[4,6,7,10,11,15,18,28,31,34,35],filter_1:35,filter_2:35,filter_def:6,filter_kei:[7,14],filter_nam:35,find:[4,11,22,28,34,35],find_duplicate_text:4,finish:[4,21,35],finnish:19,first:[4,5,10,13,19,21,26,31,35],fit:[19,23,24],fix:[4,15,35],flag:[4,5,35],flag_bas:10,flat:4,flatten:[4,24,35],flexibl:[6,7,24],float64:20,folder:20,follow:[4,8,10,11,14,15,19,20,21,22,23,24,26,28,31,35,38,39],folow:26,footbal:24,forc:[26,35],force_text:[4,23,35],forcefulli:4,form:[3,4,7,10,14,20,23,24,25,26,28,35],format:[0,4,6,10,19,20,23,26,31,35,38],former:[4,7,22,23,24,35],fortnight:22,found:[2,3,4,7,10,20,22,24,26,31,34,35],four:[28,31],frang:[4,25,26,35,38],freez:7,freeze_master_meta:7,french:19,frequenc:[4,5,8,9,10,28,31,35],freysson:0,from:[0,2,3,4,5,7,10,12,14,15,18,19,21,24,25,26,28,31,34,35,38],from_batch:[4,35],from_compon:[4,20,35],from_dichotom:[4,35],from_excel:[4,35],from_sav:7,from_set:[4,20,35],from_stack:[4,35],front:35,fulfil:4,full:[3,4,5,7,21,23,25,35,39],fullnam:31,fully_hidden_arrai:4,fun:35,further:21,futur:[7,35],geir:0,gender:[4,13,14,15,22,24,26,28,31,35,38],gener:[2,4,5,7,8,9,11,20,22,23,26,31,35],generate_report:35,german:[19,23],get:[4,7,8,11,12,13,14,22,28,31,35,37],get_batch:[4,12,13,28,31,34],get_edit_param:8,get_properti:4,get_qp_dataset:35,get_s:10,get_sig:10,get_statist:10,get_std_param:8,getter:34,give:[13,26,35],given:[4,5,6,7,10,20,26,34,35],global:[4,8,10,14,15,21,35],goe:4,going:37,grab:34,greater:4,grid:[4,35],griffith:0,group:[4,5,6,7,8,10,19,21,22,24,25,28,31,35],group_nam:6,group_target:6,grouped_ag:24,grp:10,grp_text_map:31,guid:35,gzip:7,hack:4,had:26,hand:24,handl:[0,6,7,9,10,20,31,34,35,37,39],handler:35,happen:[4,24],happend:4,has:[2,4,7,12,15,19,21,25,26,31,34,35,38],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:8,have:[4,7,10,13,20,23,24,25,26,28,34,35,38],head:[20,22,23,24,26,35,39],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,7,26,28,35,39],hidden:[4,34,35],hide:[4,15,34,35],hide_empti:35,hide_empty_item:[4,35],hide_on_i:4,hide_valu:[4,35],high:[5,10],higher:10,hmerg:[4,34,35],hockei:24,hold:[2,4,7,10,34],horizont:[4,18],household:20,how:[3,4,7,14,19,22,24,28,31,37,39],howev:[23,24,26,37,39],hrafn:0,html:[4,23,35],hub:34,ident:[4,20,24,34,35],identif:4,identifi:[4,5,23,35,38],ids:4,ignor:[4,7,10,22,24,35],ignore_arrai:4,ignore_cod:4,ignore_flag:10,ignore_item:[4,24,35],ignore_valu:[4,24],ill:23,implement:[7,10,19,22,35],impli:4,implicitli:10,impract:28,impute_method:6,incl:[7,35],includ:[2,3,4,5,7,10,13,25,26,28,31,34,35,38],inclus:[24,35],incom:[4,10,24],inconsist:[4,15,23,34,35],incorrect:35,incorrectli:35,independ:[4,10],index:[0,2,4,7,10,35],indic:[4,5,7,10,23,24,26,35],individu:[3,4,35],industri:20,infer:[7,20,24,35],info:[4,18,19],inform:[0,4,8,9,13,14,15,19,20,23,26,31,35,37,38],inherit:[11,35],inhomogen:10,init:26,initi:[4,18,35,38],inject:[4,26],innermost:8,inplac:[4,7,10,18,21,26,35],input:[4,6,7,10,11,20,35],insert:[4,13,35],insid:[2,4,9,10,19,20,22,23,35],inspect:[4,18,35],instal:[4,35],instanc:[2,3,4,7,9,10,11,14,20,21,22,23,24,28,31,34,35],instead:[4,7,10,20,25,26,34,35],instruct:[5,10,19,26,31,34,35],int64:[20,22],integ:10,integr:22,intend:7,inter:[4,6],interact:[2,3,4,21,22,35],interfac:0,interim:35,interlock:[4,18,35],intern:[2,34,35],interpret:[4,25],intersect:[4,18,22,35],intro:21,introduc:10,involv:26,iolocal:[7,20],ioutf8:7,ipython:[4,21,35],is_arrai:[31,35],is_bas:8,is_block:31,is_count:8,is_cumul:8,is_dat:35,is_delimited_set:35,is_float:35,is_g:[4,25,35],is_int:35,is_like_numer:[4,22,24,35],is_meanstest:8,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:8,is_pct:8,is_propstest:8,is_singl:35,is_stat:8,is_str:35,is_sum:8,is_weight:[8,31],isol:35,issu:[20,34,35],ist:[19,23],item:[4,10,11,13,19,20,21,22,23,24,31,35,37,38,39],item_nam:[4,35],item_no:[4,35],item_text:[4,22,35],iter:[9,22,35],its:[2,4,5,7,8,10,11,12,13,14,15,19,20,22,24,25,26,31,35,38,39],itself:[4,10],jame:0,jjda:20,jog:24,join:[4,22],json:[4,7,11,18,35],jupyt:[4,21,35],just:35,keep:[4,9,10,21,23,24,35,38],keep_bas:10,keep_cod:[10,35],keep_origin:4,keep_variable_text:[4,35],kei:[2,4,5,7,8,11,14,19,23,24,26,31,35,38],kept:[4,10,11,35],kerstin:0,keyword:[5,9,35],kind:[22,24,28,35],kite:[19,24,39],know:[14,19,22,24,37,39],kritik:31,kwarg:[4,5,8,9,35],lab:7,label:[4,5,7,14,19,20,22,23,24,26,34,35,37,38,39],lack:0,lang:19,languag:[11,18,23,35],larg:[14,21,28,31,35],last:4,lastli:34,later:[4,13,20,35],latest:[0,33,35],latter:[22,35],lead:[24,31,35],least:[4,22,23],leav:[23,24],left:[4,10,34,35],left_id:4,left_on:[4,35],legaci:4,lemonad:38,length:[4,35],less:[22,34,38],let:[23,24,28],level:[4,5,7,10,14,19,26,31,35,38],lib:[4,19,35,39],librari:[0,19,35],lift:24,like:[0,4,5,7,13,14,19,21,22,24,26,28,31,34,35,38,39],limit:[4,10,24,26,35],link:[2,4,5,7,8,9,10,11,14,27,31,35],list:[2,4,5,6,7,9,10,13,18,19,21,22,23,24,26,34,35,37,38],list_vari:35,listen:26,load:[2,3,4,7,11,35],load_cach:7,loc:31,local:[7,13,22,28,31],locat:[2,4,7,35],logic1:35,logic2:35,logic:[4,5,8,10,18,22,24,26,34,35],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:35,look:[4,13,24,26,34,35,38,39],loop:[35,38],lose:[13,24,38],lot:[4,21,22,26],low:[4,5,10,34],lower:[4,10,34,35],lower_q:10,lunch:22,machin:[4,20,35],made:2,mai:[4,25,26],main:[4,6,10,13,14,19,24,31,34,35],main_filt:35,mainli:3,major:10,mak:35,make:[4,5,22,24,28,35],make_summari:[13,35],make_templ:9,male:[22,26],manag:[0,18,26],mani:[13,28],manifest:4,manipul:[10,13,15],manual:[4,5,35,37],map:[4,5,6,7,9,10,18,20,23,35,37],mapper:[4,18,25,35,38],mapper_to_meta:4,margin:[5,10],mark:10,market:[0,20],mask:[4,18,21,22,23,35,37,38,39],mass:5,massiv:26,master:[34,35],master_meta:7,match:[4,6,7,20,24,35,39],matric:10,matrix:10,matrixcach:7,matter:[11,34],max:[7,10,31,35],max_iter:6,mdd:[0,4,20,35],mdm_lang:35,mean:[4,5,6,7,8,10,13,15,20,22,24,25,26,31,35,38],measur:[5,10],median:[6,7,10,31,35],membership:[22,35],memori:[21,35],memoryerror:35,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,34,35],merge_exist:[4,34],merge_text:[4,35],messag:35,meta:[4,5,7,8,11,12,14,15,18,20,21,22,24,26,31,34,35,37,38,39],meta_dict:[4,35],meta_edit:[4,7,15],meta_to_json:[4,35],metadata:[0,4,7,18,19,20,24,26,34,35,37,39],metaobject:35,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,34,35,37,38,39],metric:[5,7,10],mid:[5,10,22,35],middl:35,might:[7,21,23,24,34],mimic:[5,10],mimick:[5,10,22],min:[4,7,10,31,34,35],min_value_count:[4,34],minimum:5,minor:35,mismatch:35,miss:[4,6,8,14,20,22,23,24,34,37,39],missing_map:[4,35],mix:[4,24],mode:[6,7,20,35,36],modifi:[4,7,10,15,24,34,35],modu:35,modul:[0,4,7],month:22,more:[2,4,22,25,26,31,34,35],morn:22,most:[10,21,22,26,34],mous:4,move:[0,4,21,35],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,38],mul:7,multi:[5,10,19],multiindex:10,multipl:[0,3,4,5,19,22,23,25,26,35],multipli:10,multivari:10,must:[4,6,7,20,21,23,26,31,35,38],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,34,35,37,38,39],name_data:[35,39],nan:[4,10,20,22,23,24,26,28,31,35,39],nate:4,nativ:[0,4,18,35],natur:[10,21,22],necessari:[10,21,35,38],need:[4,7,9,10,21,23,24,34,35,39],neg:35,nest:[7,8,19,25,26],net:[5,7,8,10,18,27,34,35],net_1:[7,31],net_2:[7,31],net_3:7,net_map:[7,31],net_view:35,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:35,new_cod:10,new_column:4,new_d:35,new_data:7,new_data_kei:7,new_dataset:4,new_int:23,new_meta:7,new_nam:[4,23,24,34,35],new_ord:[4,21,35],new_rul:35,new_set:[4,35],new_singl:23,new_stack:2,new_text:[4,23],new_var:35,new_weight:7,newli:34,next:[4,7,11,19,31,35,39],no_data:24,no_filt:[4,7,28,31,35],non:[4,7,22,25,27,35],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,34,35,37,39],none_band:35,nonea:4,normal:[4,5,7,10,35],norwegian:19,not_al:18,not_ani:[4,10,18,35],not_count:18,notat:[5,7,8,10],note:[2,4,5,26,31],notebook:[4,21,35],notimplementederror:[15,35],now:[13,14,21,31,34,35,38],num:5,number:[4,6,10,20,22,23,24,34,35],numer:[4,5,7,18,19,23,24,31,34,35,37],numpi:[0,10,35],obei:22,object:[2,3,4,5,7,10,18,20,21,24,26,28,31,35,37],obscur:26,observ:0,obvious:31,occur:26,oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,35,37,39],often:[21,22,24],old:[4,5,7,35],old_cod:10,old_nam:35,older:35,omit:26,omnibu:34,on_var:[7,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,34,35],ones:[4,10,15,21,22,34],onli:[4,7,9,10,11,13,14,21,23,24,26,28,31,34,35,37,38],only_men:24,only_typ:[4,7],onto:26,oom:35,open:[0,11,12,34,35],oper:[4,5,20,22,24,25,26,35],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,7,10,13,14,19,22,23,31,34,35],order:[2,4,11,18,19,24,34,35],ordereddict:[3,4,7,13,28,31],organ:16,orgin:10,orient:[2,35],origi:4,origin:[4,7,10,21,24,34,35],other:[4,5,7,10,11,12,13,19,23,24,25,26,34,35,38],other_sourc:[7,31,35],otherwis:[4,34,35],our:[0,22,26],out:[5,12,14,24,31,35],outcom:4,outdat:35,output:[4,7,10,13,22,35],outsid:35,over:[4,7,9,10,35],overcod:[4,7,31],overlap:10,overview:[4,7,22,28,35],overwrit:[4,15,21,26,34,35],overwrite_margin:10,overwrite_text:4,overwritten:[4,7,35],ovlp_correc:10,own:[13,35],pack:4,packag:5,paint:[3,35],painter:35,pair:[4,5,10,18,23,26,35,37],panda:[0,3,4,6,7,10,19,20,22,26,35],pane:4,parachut:[19,24,39],parallel:7,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,34,35,38],parent:[4,19,20,35],pars:5,part:[4,8,19,21,22,26,35],parti:18,particip:20,particular:4,pass:[4,5,7,10,21,22,24,34,35],past:26,path:[2,3,4,7,20,35],path_clust:3,path_csv:20,path_data:[4,39],path_ddf:[4,20,35],path_json:20,path_mdd:[4,20,35],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:7,path_txt:20,path_xlsx:[4,35],path_xml:20,pct:4,peopl:20,per:[4,5,6,7,10,12,20,22,23,24,26,35,39],percentag:[7,8,10,31,35],perform:[4,5,7,8,10,21,26,31,34,35],perman:21,physic:35,pick:[4,24],pickl:2,pilat:24,pivot:7,place:[10,26,34,35],plai:22,plain:[0,8,20],plan:[11,13,14,19,24,28,31],pleas:[7,21,22,34,35],point:19,pointer:19,pool:10,popul:[4,11,13,14,27,31,35],portion:7,posit:[4,10,21,22,23,28,34,35],possibl:[3,4,5,7,12,13,19,22,23,25,31,34,35,38],power:0,powerpoint:[5,35],powerpointpaint:35,pptx:35,pre:[4,26,31],precis:[26,35],prefer:22,prefix:[4,7],prep:25,prepar:[3,21,23,31,34,35],present:[4,10,35],preset:[4,7],pretti:[4,26],prevent:[4,15,21,23,24,34,35],previou:[21,35],previous:[4,35],primarili:4,print:[13,22,28,31,35],prior:[4,24],prioriti:35,probabl:[19,24,35],problem:[34,35],process:[0,4,7,9,20,21,22],produc:[5,10,13,24],product:[35,38],profession:[4,35],progress:[4,35],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[34,35,39],properli:39,properti:[4,10,11,20,35],proport:[5,6,8],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,35,37],proxi:7,purpos:19,put:7,python:[4,34],q01_1:4,q01_3:4,q11:35,q11_grid:35,q12:38,q12_10:38,q12_11:38,q12_12:38,q12_13:38,q12_1:38,q12_2:38,q12_3:38,q12_4:38,q12_5:38,q12_6:38,q12_7:38,q12_8:38,q12_9:38,q12_:38,q12a:38,q12a_10:38,q12a_11:38,q12a_12:38,q12a_13:38,q12a_1:38,q12a_2:38,q12a_3:38,q12a_4:38,q12a_5:38,q12a_6:38,q12a_7:38,q12a_8:38,q12a_9:38,q12a_grid:38,q12b:38,q12b_10:38,q12b_11:38,q12b_12:38,q12b_13:38,q12b_1:38,q12b_2:38,q12b_3:38,q12b_4:38,q12b_5:38,q12b_6:38,q12b_7:38,q12b_8:38,q12b_9:38,q12b_grid:38,q12c:38,q12c_10:38,q12c_11:38,q12c_12:38,q12c_13:38,q12c_1:38,q12c_2:38,q12c_3:38,q12c_4:38,q12c_5:38,q12c_6:38,q12c_7:38,q12c_8:38,q12c_9:38,q12c_grid:38,q12d:38,q12d_10:38,q12d_11:38,q12d_12:38,q12d_13:38,q12d_1:38,q12d_2:38,q12d_3:38,q12d_4:38,q12d_5:38,q12d_6:38,q12d_7:38,q12d_8:38,q12d_9:38,q12d_grid:38,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,38],q1_2:[4,26,38],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:35,q4a_1:35,q4a_2:35,q4a_3:35,q4a_grid:35,q5_1:[19,21,22,24,35,39],q5_2:[19,21,22,24,35,39],q5_3:[19,21,22,24,35,39],q5_4:[19,21,22,24,35,39],q5_5:[19,21,22,24,35,39],q5_6:[19,21,22,24,35,39],q5_grid:39,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:39,q6_net:31,q6copi:39,q6new:39,q6new_grid:39,q6new_q6copi:39,q6new_q6copy_grid:39,q6new_q6copy_tran:39,q6new_q6copy_trans_grid:39,q7_1:[21,22,35],q7_2:[21,22,35],q7_3:[21,22,35],q7_4:[21,22,35],q7_5:[21,22,35],q7_6:[21,22,35],q7_grid:39,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:38,q_label:[4,35],qtp:38,qtype:[4,23,34,37,39],qualifi:[4,5,10],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,35,37,39],quantipyview:[1,7,35],quantiti:[10,35],queri:[2,4,6,7,18,19,24,35,39],question:[4,10,19,24,26,31,35,38],questionnair:21,quick:[4,22,35],quickli:[6,21,22,24,35],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,34,35],rake:35,rang:[4,5,18,21,35],rate:[35,38],raw:[5,10],raw_sum:10,rbase:7,read:[0,4,20,35],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,35,39],read_spss:[4,20],rebas:10,rebuild:24,rec:[4,35],receiv:38,recod:[0,4,7,18,35],recode_from_net_def:7,recode_seri:4,recoded_filt:35,recommend:24,record_numb:[13,22],reduc:[4,7,10,21,35],reduced_d:21,reduct:4,refactor:35,refer:[4,7,10,19,23,26,28,31,35],referenc:[7,13,19,26,35],reflect:[4,10,21,35],refresh:7,refus:[19,24],regard:[4,35],region:[4,35],regist:[4,22,35],regroup:[4,34],regular:[4,19,31,35],regularli:[22,23,24],reindex:4,reintroduc:35,rel:21,rel_to:8,relat:[2,8,23,26,35],relation_str:8,relationship:7,relev:[23,35],religion:22,reload:[21,35],remain:[4,5,21,26,35],rememb:37,remind:37,remov:[4,5,7,10,12,13,18,34,35],remove_data:7,remove_filt:35,remove_html:[4,35],remove_item:4,remove_valu:[4,34],renam:[4,18,24,34,35,39],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,34,35],reorder_item:[4,35],reorder_valu:4,reorgan:0,repair:[4,34,35],repair_text_edit:[4,35],repeat:[21,28],repetit:26,replac:[4,10,13,23,26,31,35],replace_i:[13,35],replace_text:[4,35],report:[0,4,5,6,34,35],reposit:[4,21,35],repres:[4,26],represent:[7,8,10,19,24,34],request:[5,7,13,21,23,26,31,35],request_view:35,requir:[4,21,23,35,39],rescal:[5,7,8,10,31],research:[0,20],reset:[4,35],reset_index:4,resid:35,resolv:34,resolve_nam:[4,34],resp:34,respect:[4,10,24,34,35,37],respond:[10,21,38],respons:[19,22,25,26,35,38],responsess:35,restor:[4,7,21,35],restore_item_text:4,restore_meta:7,restrict:[4,5,7,10,19,21,22,35],result:[3,4,5,7,9,10,16,20,22,23,24,26,28,35],result_onli:10,retain:9,retriev:10,revers:[24,25],revert:[4,21,35],right:[4,34,35],right_id:4,right_on:[4,35],rim:[1,4,35],roll:4,roll_up:4,rollback:[18,35],rolled_up:4,round:6,row:[4,5,10,18,20,22,35,39],row_id:4,row_id_nam:4,rule:[4,35,39],run:[4,7,10,15,24,28,31,35],safe:[4,23],safeguard:4,sai:26,same:[3,4,7,13,19,20,22,26,28,34,35,38,39],sampl:[5,8,10,34,35],sample_s:14,sandbox:35,satisfi:35,sav:[4,7,20],save:[2,3,4,7,21,28,35],savepoint:18,scalar:35,scale:[5,6,10,19,35,38],scan:4,scenario:39,scheme:[4,6,19,35],scratch:[18,35],script:4,search:4,second:[4,5,10,15,31],sect:4,section:[8,10,11,14,21,26],see:[13,21,24,26,28,31,35],seen:[26,39],segemen:26,segment:18,select:[4,7,9,13,14,21,22,31,35,38],select_text_kei:4,self:[2,4,7,10,26,28,35],sem:[7,10],semi:34,sensit:[4,34],separ:[4,26,37],septemb:33,sequenc:4,seri:[4,19,22,26,35],serial:2,session:[21,35],set:[3,4,5,6,7,9,10,11,12,13,18,19,20,22,23,24,26,28,34,35,37],set_cell_item:14,set_col_text_edit:35,set_column_text:35,set_dim_suffix:35,set_encod:4,set_factor:4,set_item_text:[4,23,35],set_languag:14,set_mask_text:35,set_miss:[4,35],set_opt:35,set_param:10,set_properti:[4,15],set_sigtest:[14,31,35],set_target:6,set_text_kei:4,set_unwgt_count:35,set_val_text_text:35,set_value_text:[4,15,23,35],set_variable_text:[4,15,23,35],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,35],setup:[4,10,13,35],sever:[5,22,38],shape:[4,21,31],share:[4,19],sheet:[4,35],shop:4,short_item_text:35,shorten:[4,35],shorthand:[4,5],shortnam:[5,8],should:[3,4,7,14,21,22,26,34,35,38,39],show:[4,10,13,19,21,22,31,35],shown:[4,10,34,35],side:[9,35],sig:[5,7,10,14,35],siglevel:35,signific:[5,10,11,27],significancetest:35,sigproperti:35,similar:[28,38],similarli:[22,23],similiar:23,simpl:[5,6,10,19,25,35,37],simpli:[4,22,23,24,25,31,34,35],simplifi:[24,26],sinc:[10,26,31,39],singl:[3,4,7,19,20,21,22,23,24,26,34,35,37,39],sit:26,six:22,size:[5,8,10,21,22,31,35],skip:[22,23,31,35],skip_item:35,slice:[4,8,15,18,35],slicer:[4,18,22,24,35],slicex:4,small:[5,35],snack:22,snapshot:[4,21,35],snowboard:[19,24,39],soccer:24,social:0,softwar:[0,4,5,10,20,34],solut:35,solv:35,some:[13,14,15,22,25,26,34,35,38],someth:38,sometim:[21,28,34],soon:35,sorri:0,sort:[4,15,35],sort_by_weight:4,sortx:4,sourc:[0,4,7,19,20,22,23,35,39],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:8,specfic:14,special:[0,11,14,19,28,31,35],specif:[3,4,5,7,8,10,11,13,14,15,19,21,23,35,39],specifi:[2,4,5,6,7,10,13,20,23,31,35,37],speed:35,spell:[4,34],split:[4,7,13,35],split_view_nam:7,sport:[20,22],spreadsheet:0,spss:[0,4,10,18,35],spss_limit:[4,35],squar:7,stack:[1,2,3,4,5,8,11,13,14,27,31,35],stage:[4,35],standalon:18,standard:[8,10,20,34],standardli:24,start:[4,18,23,24,26],start_meta:[4,35],start_tim:22,stat:[4,5,7,10,31,35],state:[4,15,18,24,35],statement:[4,5,19,25,26],statisfi:35,statist:[0,4,5,7,8,10,18,27,28,35],std_paramet:8,stddev:[7,10,31,35],ste:35,stem:35,step:[31,37],still:[26,35],store:[4,5,7,11,12,13,19,21,24,28,31,35],store_cach:7,str:[3,4,5,6,7,8,9,10,24,34,35,37,38],str_tag:[4,34],strict:[4,35],strict_select:9,strictli:23,string:[2,4,6,7,8,10,19,20,21,22,23,24,25,34,35],strip:35,structur:[0,4,6,7,9,11,13,19,20,21,24,28,35,38],studi:35,style:[4,7],sub:[7,31],subclass:[2,11],subclasss:15,sublist:35,subset:[4,9,18,22,24,35],subset_d:4,substr:[4,34],subtl:35,subtyp:[19,35,39],suffix:[4,5,24,34,35,39],sum:[4,5,7,8,10,27,35],summar:[4,5,10,35],summari:[4,5,6,7,8,10,13,22,31,34,35],summaris:7,summat:10,suppli:24,supporintg:7,support:[0,7,8,18,19,22,23,24,34,35,39],surf:[19,24,39],survei:21,sv_se:[31,35],swap:[7,8,10,35],swedish:[19,35],swim:24,syntax:35,sys:4,tab:20,tabl:[0,7],tabul:[5,13,28],tag:[4,23,34,35],take:[4,5,7,11,22,24,25,26,34,35,38],taken:[4,7,14,15,24,34,35],target:[4,6,18,23,34,35],target_item:4,task:22,team:22,temp:4,templat:[5,9,35],temporari:[4,35],temporarili:4,ten:26,tend:4,term:[7,23,35],termin:35,test:[2,4,5,8,10,11,22,27,34,35,37],test_cat_1:37,test_cat_2:37,test_cat_3:37,test_tot:[5,10,35],test_var:[34,35,37],testtyp:10,text1:20,text:[4,5,7,8,14,18,20,21,22,24,26,31,35,37,39],text_kei:[3,4,7,11,18,22,23,31,35],text_label:[4,35],text_prefix:7,textkei:[4,35],than:[4,22,24,34,35],thei:[4,10,13,14,20,25,26,31,34,35],them:[4,5,13,20,22,26,31,34,35],themselv:[4,10],therefor:[4,5,24,34,35],thi:[2,3,4,5,6,7,10,13,14,15,20,21,22,23,24,26,28,31,35,38,39],third:18,thorugh:24,those:4,three:[4,21,22,24,26,35,37],threshold:5,through:[2,3,4,9],throughout:[4,19,20,35],thu:6,time:[7,19,21,22],titl:13,tks:35,to_arrai:[4,34,35,38],to_delimited_set:[4,35],to_df:10,to_excel:7,todo:[4,5,6,7,9,10],togeth:[3,4,7,19,21],toggl:7,too:35,tool:[5,20,24,25],top2:31,top3:7,top:26,topic:[19,34,35],total:[4,5,6,10,13,35],toward:35,tracker:35,tradit:10,transfer:35,transform:[0,4,5,10,18,35],translat:23,transpos:[4,13,24,31,39],transpose_arrai:13,transposit:24,treat:[4,10,25,31],tree:28,treshhold:10,trigger:4,tstat:10,tupl:[4,8,23,24,35,37],turn:19,two:[4,5,10,13,19,21,23,28,31,34,35],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,34,35,37,39],type_nam:7,typic:26,ultim:4,unabbrevi:25,unattend:4,unbind:4,uncod:[4,35],uncode_seri:4,uncodit:5,unconditi:10,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:7,uni:[5,10],unicod:7,unifi:[4,35],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,7,24,28,31,35],unique_id:[4,22],unique_kei:[4,35,38],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:10,unqiu:24,unrol:[4,31,35],untouch:[23,34],unusu:35,unwant:[4,35],unweight:[7,10,31,35],unweighted_bas:[7,31,35],unwgt:35,upcom:33,updat:[4,8,9,10,23,34,35],update_axis_def:[10,35],update_exist:[4,35],upon:19,upper:[4,35],upper_q:10,uppercas:34,usag:[23,34,35],use:[0,2,4,5,7,10,12,13,19,20,21,22,23,24,26,34,35,36],use_ebas:10,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,34,35],useful:[21,22,35],user:[2,4,14,35,37],userwarn:[35,37],uses:[4,10,35],using:[0,2,3,4,6,7,19,20,21,24,25,26,28,31,34,35,39],usual:19,utf8:34,utf:7,val:4,val_text_sep:4,valid:[4,6,7,14,19,24,26,31,35,37],valid_cod:35,valid_tk:[11,35],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,34,35,36,39],value_count:[4,22,35],value_map:38,value_text:[4,22,35],valueerror:[4,21,23,34,35],var_exist:[22,35],var_grid:38,var_nam:[35,38],var_suffix:4,varcoeff:10,vari:22,variabl:[0,4,5,6,7,8,10,11,18,19,20,23,27,34,35,37,38,39],variable_typ:7,variables_from_set:35,varianc:10,variant:[22,34],varibal:38,varibl:35,variou:[5,14,22,28,31],varlist:[4,35],varnam:[4,35],vector:10,verbatim:[11,34],verbos:[4,7,25,31,35],veri:[19,23,24,35],versa:10,version2:24,version:[4,5,7,10,19,21,23,24,26,34,35],versu:31,vertic:[4,18],via:[0,4,5,7,21,22,23,24,31,35],vice:10,view:[1,2,3,4,5,7,9,10,14,16,22,27,28,35],view_kei:31,view_name_not:10,viewmanag:35,viewmapp:[1,7],viewmeta:8,visibl:[31,35],vmerg:[4,34,35],wai:[7,12,13,19,21,22,23,26,31,35,36],wait:21,want:[21,24,26],warn:[4,31,34,35],water:38,wave:21,weak:[4,34],weak_dup:4,week:22,weight:[0,4,6,7,8,10,11,12,22,24,31,34,35],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,weigth:4,well:[4,10,20,22,25,26,31,34,35,39],went:35,were:[26,34,35],wgt:35,what:[16,19,20,24,26,27,35,36,39],whatev:[4,26],when:[4,5,7,10,20,21,23,24,26,35,39],where:[2,3,4,10,24,25,26],whether:[5,10],which:[4,5,7,10,11,13,14,15,22,23,24,26,28,31,34,35,38],whole:[4,35],whose:[4,7,35],wide:35,wil:4,wildcard:26,window:20,windsurf:[19,24,39],wise:[4,31],witch:35,within:[4,10],without:[34,35],women:15,work:[4,11,21,23,31,35],workbook:3,workspac:35,world:20,would:[4,19,24,26,35],wouldn:[19,24],wrap:35,wrapper:[4,10,34],write:[4,7,20,21,35,39],write_dimens:[4,34,35],write_quantipi:[4,21],write_spss:[4,20],writen:38,written:[21,35],wrong:35,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:10,xks:[4,7,31,35],xlsx:4,xml:[4,20],xsect:10,xtotal:4,y_filter:35,y_kei:[14,28,31],y_on_i:[13,28,31,35],year:[19,22,39],yes:20,yet:34,yield:22,yks:[4,35],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,35,38,39],younger:24,your:[4,19,20,21,24,26,35],ysect:10},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","Stack","View","ViewMapper","quantify.engine","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Documentation","Release notes","Upcoming (September)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,39],Using:20,about:38,access:39,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:35,arrai:[13,19,24,38,39],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,37],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:39,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,37,39],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,39],datafram:20,dataset:[4,15,19,21,23,38,39],deciph:20,definit:31,deriv:26,derot:38,descript:31,detail:26,dice:22,differ:37,dimens:[20,39],document:32,edit:23,end:13,engin:[10,29],exampl:26,exist:[22,25],extend:23,featur:0,fill:26,fillna:26,filter:[14,21],from:[20,23],has_al:25,has_ani:25,has_count:25,horizont:21,how:[36,38],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:34,link:28,list:25,load:12,logic:25,manag:21,map:19,mapper:26,mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:39,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[33,35],numer:26,object:[19,22,23],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,process:18,properti:14,python:0,quantifi:10,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,releas:[33,35],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:34,set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:36,special:13,spss:20,stack:[7,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:34,use:38,valu:[19,22,23,37],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[8,17,31],viewmapp:9,wai:37,weight:14,what:[17,28,38]}}) \ No newline at end of file +Search.setIndex({docnames:["index","sites/api_ref/00overview","sites/api_ref/Chain","sites/api_ref/Cluster","sites/api_ref/DataSet","sites/api_ref/QuantipyViews","sites/api_ref/Rim_scheme","sites/api_ref/Stack","sites/api_ref/View","sites/api_ref/ViewMapper","sites/api_ref/quantify_engine","sites/lib_doc/batch/00_overview","sites/lib_doc/batch/01_create_load","sites/lib_doc/batch/02_variables","sites/lib_doc/batch/03_properties","sites/lib_doc/batch/04_subclass","sites/lib_doc/builds/00_overview","sites/lib_doc/builds/01_chains","sites/lib_doc/dataprocessing/00_overview","sites/lib_doc/dataprocessing/01_components","sites/lib_doc/dataprocessing/02_io","sites/lib_doc/dataprocessing/02a_management","sites/lib_doc/dataprocessing/03_inspection","sites/lib_doc/dataprocessing/04_editing","sites/lib_doc/dataprocessing/05_transforming","sites/lib_doc/dataprocessing/06_logics","sites/lib_doc/dataprocessing/07_custom_recoding","sites/lib_doc/engine/00_overview","sites/lib_doc/engine/01_links_stacks","sites/lib_doc/engine/02_quantity","sites/lib_doc/engine/03_test","sites/lib_doc/engine/04_agg_methods","sites/release_notes/00_overview","sites/release_notes/01_latest","sites/release_notes/02_archive","sites/release_notes/03_how_to_snippets","sites/release_notes/how_to_snippets/create_categorical_meta","sites/release_notes/how_to_snippets/derotate","sites/release_notes/how_to_snippets/dimensions_comp"],envversion:52,filenames:["index.rst","sites\\api_ref\\00overview.rst","sites\\api_ref\\Chain.rst","sites\\api_ref\\Cluster.rst","sites\\api_ref\\DataSet.rst","sites\\api_ref\\QuantipyViews.rst","sites\\api_ref\\Rim_scheme.rst","sites\\api_ref\\Stack.rst","sites\\api_ref\\View.rst","sites\\api_ref\\ViewMapper.rst","sites\\api_ref\\quantify_engine.rst","sites\\lib_doc\\batch\\00_overview.rst","sites\\lib_doc\\batch\\01_create_load.rst","sites\\lib_doc\\batch\\02_variables.rst","sites\\lib_doc\\batch\\03_properties.rst","sites\\lib_doc\\batch\\04_subclass.rst","sites\\lib_doc\\builds\\00_overview.rst","sites\\lib_doc\\builds\\01_chains.rst","sites\\lib_doc\\dataprocessing\\00_overview.rst","sites\\lib_doc\\dataprocessing\\01_components.rst","sites\\lib_doc\\dataprocessing\\02_io.rst","sites\\lib_doc\\dataprocessing\\02a_management.rst","sites\\lib_doc\\dataprocessing\\03_inspection.rst","sites\\lib_doc\\dataprocessing\\04_editing.rst","sites\\lib_doc\\dataprocessing\\05_transforming.rst","sites\\lib_doc\\dataprocessing\\06_logics.rst","sites\\lib_doc\\dataprocessing\\07_custom_recoding.rst","sites\\lib_doc\\engine\\00_overview.rst","sites\\lib_doc\\engine\\01_links_stacks.rst","sites\\lib_doc\\engine\\02_quantity.rst","sites\\lib_doc\\engine\\03_test.rst","sites\\lib_doc\\engine\\04_agg_methods.rst","sites\\release_notes\\00_overview.rst","sites\\release_notes\\01_latest.rst","sites\\release_notes\\02_archive.rst","sites\\release_notes\\03_how_to_snippets.rst","sites\\release_notes\\how_to_snippets\\create_categorical_meta.rst","sites\\release_notes\\how_to_snippets\\derotate.rst","sites\\release_notes\\how_to_snippets\\dimensions_comp.rst"],objects:{"quantipy.Chain":{concat:[2,2,1,""],copy:[2,2,1,""],describe:[2,2,1,""],load:[2,3,1,""],save:[2,2,1,""]},"quantipy.Cluster":{add_chain:[3,2,1,""],bank_chains:[3,2,1,""],load:[3,3,1,""],merge:[3,2,1,""],save:[3,2,1,""]},"quantipy.DataSet":{add_meta:[4,2,1,""],all:[4,2,1,""],any:[4,2,1,""],band:[4,2,1,""],by_type:[4,2,1,""],categorize:[4,2,1,""],clear_factors:[4,2,1,""],clone:[4,2,1,""],code_count:[4,2,1,""],code_from_label:[4,2,1,""],codes:[4,2,1,""],codes_in_data:[4,2,1,""],compare:[4,2,1,""],convert:[4,2,1,""],copy:[4,2,1,""],copy_array_data:[4,2,1,""],create_set:[4,2,1,""],crosstab:[4,2,1,""],cut_item_texts:[4,2,1,""],data:[4,2,1,""],derive:[4,2,1,""],derotate:[4,2,1,""],describe:[4,2,1,""],dichotomize:[4,2,1,""],dimensionize:[4,2,1,""],dimensionizing_mapper:[4,2,1,""],drop:[4,2,1,""],drop_duplicates:[4,2,1,""],duplicates:[4,2,1,""],empty:[4,2,1,""],empty_items:[4,2,1,""],extend_items:[4,2,1,""],extend_values:[4,2,1,""],factors:[4,2,1,""],filter:[4,2,1,""],find:[4,2,1,""],find_duplicate_texts:[4,2,1,""],flatten:[4,2,1,""],force_texts:[4,2,1,""],from_batch:[4,2,1,""],from_components:[4,2,1,""],from_excel:[4,2,1,""],from_stack:[4,2,1,""],fully_hidden_arrays:[4,2,1,""],get_batch:[4,2,1,""],get_property:[4,2,1,""],hide_empty_items:[4,2,1,""],hiding:[4,2,1,""],hmerge:[4,2,1,""],interlock:[4,2,1,""],is_like_numeric:[4,2,1,""],is_nan:[4,2,1,""],item_no:[4,2,1,""],item_texts:[4,2,1,""],items:[4,2,1,""],link:[4,2,1,""],merge_texts:[4,2,1,""],meta:[4,2,1,""],meta_to_json:[4,2,1,""],min_value_count:[4,2,1,""],names:[4,2,1,""],order:[4,2,1,""],parents:[4,2,1,""],populate:[4,2,1,""],read_ascribe:[4,2,1,""],read_dimensions:[4,2,1,""],read_quantipy:[4,2,1,""],read_spss:[4,2,1,""],recode:[4,2,1,""],remove_html:[4,2,1,""],remove_items:[4,2,1,""],remove_values:[4,2,1,""],rename:[4,2,1,""],rename_from_mapper:[4,2,1,""],reorder_items:[4,2,1,""],reorder_values:[4,2,1,""],repair:[4,2,1,""],repair_text_edits:[4,2,1,""],replace_texts:[4,2,1,""],resolve_name:[4,2,1,""],restore_item_texts:[4,2,1,""],revert:[4,2,1,""],roll_up:[4,2,1,""],save:[4,2,1,""],select_text_keys:[4,2,1,""],set_encoding:[4,4,1,""],set_factors:[4,2,1,""],set_item_texts:[4,2,1,""],set_missings:[4,2,1,""],set_property:[4,2,1,""],set_text_key:[4,2,1,""],set_value_texts:[4,2,1,""],set_variable_text:[4,2,1,""],set_verbose_errmsg:[4,2,1,""],set_verbose_infomsg:[4,2,1,""],slicing:[4,2,1,""],sorting:[4,2,1,""],sources:[4,2,1,""],split:[4,2,1,""],start_meta:[4,3,1,""],subset:[4,2,1,""],take:[4,2,1,""],text:[4,2,1,""],to_array:[4,2,1,""],to_delimited_set:[4,2,1,""],transpose:[4,2,1,""],unbind:[4,2,1,""],uncode:[4,2,1,""],undimensionize:[4,2,1,""],undimensionizing_mapper:[4,2,1,""],unify_values:[4,2,1,""],unroll:[4,2,1,""],update:[4,2,1,""],validate:[4,2,1,""],value_texts:[4,2,1,""],values:[4,2,1,""],variables:[4,2,1,""],vmerge:[4,2,1,""],weight:[4,2,1,""],write_dimensions:[4,2,1,""],write_quantipy:[4,2,1,""],write_spss:[4,2,1,""]},"quantipy.QuantipyViews":{"default":[5,2,1,""],coltests:[5,2,1,""],descriptives:[5,2,1,""],frequency:[5,2,1,""]},"quantipy.Quantity":{calc:[10,2,1,""],count:[10,2,1,""],exclude:[10,2,1,""],filter:[10,2,1,""],group:[10,2,1,""],limit:[10,2,1,""],normalize:[10,2,1,""],rebase:[10,2,1,""],rescale:[10,2,1,""],summarize:[10,2,1,""],swap:[10,2,1,""],unweight:[10,2,1,""],weight:[10,2,1,""]},"quantipy.Rim":{add_group:[6,2,1,""],group_targets:[6,2,1,""],report:[6,2,1,""],set_targets:[6,2,1,""],validate:[6,2,1,""]},"quantipy.Stack":{add_data:[7,2,1,""],add_link:[7,2,1,""],add_nets:[7,2,1,""],add_stats:[7,2,1,""],add_tests:[7,2,1,""],aggregate:[7,2,1,""],apply_meta_edits:[7,2,1,""],cumulative_sum:[7,2,1,""],describe:[7,2,1,""],freeze_master_meta:[7,2,1,""],from_sav:[7,3,1,""],load:[7,3,1,""],recode_from_net_def:[7,3,1,""],reduce:[7,2,1,""],refresh:[7,2,1,""],remove_data:[7,2,1,""],restore_meta:[7,2,1,""],save:[7,2,1,""],variable_types:[7,2,1,""]},"quantipy.Test":{get_se:[10,2,1,""],get_sig:[10,2,1,""],get_statistic:[10,2,1,""],run:[10,2,1,""],set_params:[10,2,1,""]},"quantipy.View":{get_edit_params:[8,2,1,""],get_std_params:[8,2,1,""],has_other_source:[8,2,1,""],is_base:[8,2,1,""],is_counts:[8,2,1,""],is_cumulative:[8,2,1,""],is_meanstest:[8,2,1,""],is_net:[8,2,1,""],is_pct:[8,2,1,""],is_propstest:[8,2,1,""],is_stat:[8,2,1,""],is_sum:[8,2,1,""],is_weighted:[8,2,1,""],meta:[8,2,1,""],missing:[8,2,1,""],nests:[8,2,1,""],notation:[8,2,1,""],rescaling:[8,2,1,""],spec_condition:[8,2,1,""],weights:[8,2,1,""]},"quantipy.ViewMapper":{add_method:[9,2,1,""],make_template:[9,2,1,""],subset:[9,2,1,""]},Chain:{filename:[2,0,1,""]},quantipy:{Chain:[2,1,1,""],Cluster:[3,1,1,""],DataSet:[4,1,1,""],QuantipyViews:[5,1,1,""],Quantity:[10,1,1,""],Rim:[6,1,1,""],Stack:[7,1,1,""],Test:[10,1,1,""],View:[8,1,1,""],ViewMapper:[9,1,1,""]}},objnames:{"0":["py","attribute","Python attribute"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","staticmethod","Python static method"],"4":["py","classmethod","Python class method"]},objtypes:{"0":"py:attribute","1":"py:class","2":"py:method","3":"py:staticmethod","4":"py:classmethod"},terms:{"0x0000000019ae06d8":[28,31],"\u00ecnt":[20,31],"\u00ectem":38,"boolean":[4,6,7,18,34],"case":[0,4,5,6,7,10,12,13,15,18,20,23,24,26,28,33,34,36,37],"class":[2,3,4,5,6,7,8,9,10,11,22,33,34],"default":[3,4,5,7,8,9,10,13,18,20,21,23,24,33,34,37,38],"export":[0,3,4,20,34],"final":26,"float":[4,5,7,10,19,20,21,22,23,24,31,33,34],"function":[4,7,20,25,26,28,31,34],"import":[4,10,11,15,20,22,25,26],"int":[4,5,7,10,19,20,21,22,23,24,25,33,34],"long":[25,34],"m\u00fcller":0,"new":[3,4,5,7,9,10,12,21,23,24,26,31,33,34,37,38],"null":31,"return":[3,4,5,6,7,8,9,10,21,22,24,26,33,34],"short":[9,19,21,24,34],"sigur\u00f0sson":0,"static":[2,3,4,7],"switch":[24,33,37],"true":[4,5,6,7,9,10,13,20,21,22,24,25,26,31,34,37,38],"try":[4,7,23,34],"var":[4,10,15,33,34,37],"while":[4,15,19,23,24,33,34],Adding:[11,18,31],Age:[24,26],Being:15,But:[13,23],Das:[19,23],For:[4,5,13,14,20,22,23,24,25,26,28,31,33],NPS:31,Not:[19,24,31,34],One:[4,13,26],That:[15,19,22,24],The:[2,3,4,5,7,8,9,10,11,13,14,15,18,20,21,22,23,24,25,27,31,33,34,35,36,37],Their:5,Then:[21,34],There:[13,19,21,22,31,34],These:[15,28,31,34],Use:[4,10,24,34],Uses:4,Using:[7,18,21,23],Will:[4,5,7,10,34],With:[13,22,23,24,26,31],Yes:[20,34],__init__:[12,34],__setitem__:34,_band:4,_batch:[7,31],_cumsum:[31,34],_data:[4,11,21],_dimensions_suffix:34,_get_chain:34,_grid:[34,37],_intersect:[28,31],_meta:[4,11,12,13,22,28,38],_missingfi:10,_net:34,_rc:34,_rec:[4,24,34],_remove_html:4,_request_view:34,_suffix:4,_sum:31,_tran:4,abbrevi:25,abl:[5,28,34],about:[13,19,22,35],abov:[24,31,38],absorb:34,accept:[23,26,34],access:[4,7,19,20,22,34,35],accessor:22,accid:[21,34],accommod:14,accompani:[19,34],accord:[4,7,10,26,31],accordingli:26,account:[4,5,34],achiev:21,across:[10,22,34],act:[5,24,28],activ:[19,20,24,34],add:[3,4,5,7,9,13,14,23,24,28,31,33,34,36],add_batch:[12,31],add_chain:3,add_data:7,add_filt:[14,34],add_group:6,add_i:13,add_link:[7,28,31],add_meta:[4,23,33,34,36,38],add_method:9,add_net:[7,31,34],add_open_end:[13,33,34],add_stat:[7,31,34],add_test:[7,31,34],add_tot:34,add_x:[13,31],add_y_on_i:[13,28,31,34],adddit:20,added:[4,7,12,13,14,23,24,28,31,34,37],adding:[4,14,18,20,31,34,38],addit:[4,13,19,22,31,34],addition:[19,33,34],adjust:[33,34],adopt:13,aerob:24,affect:33,affix:7,after:[4,7,10,21,26,28,31,34],afternoon:22,again:[22,23,31],against:[4,5,10,13,34],age:[4,13,14,22,24,26,28,31,34],age_band:24,age_cb:26,age_grp_rc:26,age_rec:4,age_xb:26,age_xb_1:26,agegrp:[4,34],agg:[8,13,31],aggnam:8,aggreg:[0,2,3,4,5,7,8,9,10,11,14,16,33,34],aim:[0,34],alasdair:0,alert:33,alexand:0,algorithm:[4,5,10,34],alia:[4,21],align:34,all:[2,3,4,5,7,10,11,12,13,14,19,20,21,22,23,24,25,26,28,31,33,34,36,37],allign:34,allow:[4,14,15,23,24,31,33,34],alon:[22,34],along:[2,10],alongsid:[7,21,31],alphanumer:4,alreadi:[4,12,14,19,24,26,34],also:[4,7,10,11,12,13,14,22,23,24,26,28,31,33,34,36,37],altern:[4,23,31,36],although:22,alwai:[4,15,23,24,26,31,33,34,38],amount:[4,14,21,28,31],analysi:[0,34],ani:[4,5,7,8,10,13,19,21,22,23,25,26,33,34],anim:[4,19],anoth:[7,13,23,24,26,34],answer:[4,7,10,19,22,23,24,34],anymor:[33,34],anyth:26,anywher:7,api:4,appear:[4,34,36],append:[4,5,18,31,37],appli:[4,5,7,9,10,15,20,21,22,24,28,31,34,38],applic:[4,24,26],apply_edit:4,apply_meta_edit:7,apporach:21,approach:36,appropri:[4,5],arab:34,arbitrari:7,arbitrarili:[4,21,25,26],archiv:32,argument:[5,9,11,20,23,24,34],aris:28,arithmet:10,around:[4,34],arrai:[4,7,10,11,18,21,22,23,31,33,34,35,36],array_item:34,array_var:38,array_var_1:38,array_var_2:38,array_var_3:38,array_var_grid:38,arriv:10,as_addit:[13,34],as_delimited_set:34,as_df:10,as_float:34,as_int:34,as_singl:34,as_str:34,as_typ:[4,34],ascend:4,ascii:4,ascrib:[0,4,18],asid:24,ask:[19,24],askia:[5,10],aspect:34,assess:[0,37],assign:[4,6,34,36],assignd:24,associ:[2,7],assum:[4,10,28,37],attach:[4,7,21,23,34],attempt:[4,34],attribut:[2,4,11,22,28,31,34],audit:34,auto:[4,7,33],autom:[0,7],automat:[4,13,14,20,22,23,24,31,33,34,38],auxiliari:10,avail:[4,33],avoid:26,axes:4,axi:[2,3,4,5,7,8,10,15,19,22,34],axis_edit:[4,34],b_d:4,b_filter:34,b_name:31,back:[4,5,37],badli:[4,34],band:[4,7,18,34],band_numer:34,bank:3,bank_chain:3,bar:34,base:[4,5,7,8,9,10,18,20,22,31,34],base_al:10,base_text:15,based_on:[4,34],baselin:9,basi:26,basic:[20,22,25,27,34],basketbal:24,batch1:[12,13,28,31],batch2:[12,13,28,31],batch3:31,batch4:31,batch5:31,batch6:31,batch:[0,4,7,15,28,31,33,34],batch_nam:[4,7],batchnam:[11,13],bchain:3,becaus:[24,26,34],becom:[4,10,26,34],been:[4,7,10,13,21,24,26,34,38],beer:37,befor:[4,7,10,13,14,24,26,31,34],begin:[4,26],behaviour:[5,8,22,26,34,38],being:[4,24,38],belong:[4,11,12,13,31,33,34,37],below:[4,10,13,26,34],benefici:21,benefit:26,better:28,between:[4,10,13,31,33,34],bia:10,big:21,binari:34,bird:19,birgir:0,birth_dai:22,birth_month:22,birth_year:22,bivari:5,blacklist:[4,34],blank:36,blueprint:34,board:[19,24,38],bool:[4,5,7,9,10,22,37],border:4,both:[4,7,12,13,19,20,21,23,24,26,28,33,34,38],bottom3:7,bottom:34,bound:34,bracket:[7,34],brand:37,break_bi:[13,33],breakfast:22,brief:34,broader:19,buchhamm:0,bug:34,bugfix:34,build:[0,3,4,5,10,15,18,19,34],built:[14,31],bunch:34,by_nam:[4,34],by_typ:[4,21,22,34],bytestr:2,cach:[4,7],calc:[5,7,8,10,31,34],calc_onli:[5,31],calcul:[4,5,7,10,14,27,33,34],call:[4,7,21,22,24,28,34,38],came:34,can:[2,4,5,7,10,11,12,13,14,15,19,20,21,22,23,24,25,26,28,31,33,34,36,37,38],cannot:[26,34],cap:6,carefulli:4,carri:5,case1:37,case2:37,case3:37,casedata:[4,20],cast:34,cat:[4,19,23,33,34,36,38],cat_nam:[7,34],categor:[4,5,7,18,22,23,27,34,35],categori:[4,5,7,19,22,23,33,34,36],categorized_nam:[4,34],caught:34,caus:[33,34],caution:4,cave:[19,24,38],cbase:[7,31,34],cbase_gross:34,cell:[5,10,11,26,34],cell_item:14,cellitem:34,central:5,certain:[4,22,34],chain:[0,1,3,16,22,34],chainmanag:33,chainnam:2,chang:[4,6,7,10,18,21,24,34],charact:[4,23,25],characterist:[19,24],chart:[0,34],check:[4,5,7,10,22,26,33,34],check_dup:34,checking_clust:7,choic:0,choos:5,clariti:25,classmethod:4,clean:[0,4,7,21,24,34],clean_text:[23,34],clean_up:[4,34],clear:34,clear_factor:4,client:34,clone:[4,18],close:12,cluster:[0,1,2,7,13,14,15,34],code:[4,5,6,7,8,10,18,19,22,23,24,31,33,34,36,38],code_count:[4,22,25,34],code_from_label:[4,34],code_map:4,codes_from_nam:[4,34],codes_in_data:[4,22,34],cola:37,collect:[4,5,7,11,13,14,19,21,22,24,27,31,34],collect_cod:[7,34],colour:34,coltest:[5,7,9],column:[2,4,5,6,7,8,10,18,20,22,23,24,26,31,33,34,37,38],combin:[4,5,7,16,22,25,26,28,31,34],combind:37,come:34,comma:25,common:[8,19,24,34],commun:7,compabl:4,compar:[4,5,10,33,34],comparison:10,compat:[4,7,20,34,35],compatibilti:38,complet:[4,10,34],complex:[4,5,6,18,19,21,22,26,31,34],compli:[4,26],complic:18,compon:[4,5,7,8,11,15,18,21,22,23,24,34],compos:7,compound:3,comprehens:26,compress:7,comput:[0,4,5,7,9,10,27,28,31,34],concat:[2,22,34],concaten:[2,26],concern:31,cond_map:4,condit:[4,8,10,18,21,22,25,33,34],confirm:7,conflict:34,conjunct:34,connect:[4,7,12,13,34,38],consequ:[28,36],consid:[4,5,7,10,24,34],consist:[4,10,11,19,21,23,24,34],constitut:13,constrcut:7,construct:[4,11,13,14,19,21,23,28,31,34,38],contain:[0,2,3,4,7,8,9,10,11,13,14,19,21,22,23,24,28,31,34,37],content:[21,28],context:[19,23],contrast:[19,24],contributor:0,control:[4,5,7,8,10,23,24,26,34],convcrit:6,convent:[4,10,34,38],convers:[4,18,33,34],convert:[0,4,10,20,24,33,34,38],coordin:9,cope:28,copi:[2,4,9,12,18,21,23,26,34,38],copy_array_data:4,copy_batch:12,copy_d:21,copy_data:[4,24],copy_from:[4,23,34],copy_not:[4,34],copy_of_batch1:12,copy_onli:[4,34],copy_to:[4,23,34],core:[5,20,22,25,34,36],correct:[10,23,34],correctli:34,correspond:[4,5,10,22,23,24,34,36,38],correspons:34,corrupt:34,could:[23,24,26,34],count:[4,5,7,8,10,31,33,34,37],count_not:[4,22,34],count_onli:[4,22,34],counterpart:34,counts_cumsum:[31,34],counts_sum:31,cpickl:2,crash:34,creat:[2,3,4,6,7,10,11,13,14,15,16,18,20,22,24,28,31,33,34,35],create_set:[4,34],creation:[10,18,23,34],cross:[13,28,31,34],crossbreak:28,crosstab:[4,24],crunch:34,csv:[0,4,7,11,18],cum_sum:10,cumul:[7,8,10,27,34],cumulative_sum:[7,31],current:[0,2,4,7,24,34],custom:[4,7,16,18,20,24,34],custom_text:[7,31],customis:34,customiz:0,cut:[4,21],cut_item_text:4,cwi_filt:10,cycl:[4,9],dai:[22,26],danish:19,data:[2,4,5,6,7,8,10,11,14,15,20,21,23,24,28,31,33,34,35,37],data_df:[4,34],data_kei:[4,7,14,28,34],datafil:[4,34],datafram:[3,4,6,7,8,10,18,19,21,22,23,28,31,33,34],datakei:34,dataset:[0,1,7,11,12,13,18,20,22,24,28,31,33,34,35,36],dataset_left:4,dataset_right:4,datasmoothi:0,datast:33,date:[4,7,19,20,21,22,23,24,34],dates_as_str:20,ddf:[0,4,34],deafult:[5,10],deal:20,decim:4,deciph:[0,18],deck:0,decod:[7,33],decode_str:7,decor:34,deep:4,deepcopi:34,defin:[2,3,4,7,10,13,14,19,21,23,28,31,33,34,36],definit:[2,4,5,7,10,14,19,24,26,27,28,34],definiton:[9,19,34],defint:[0,2,4,5,6,7,10,14,19,34],defintit:[21,38],defintiton:23,del:34,deleg:26,delet:[4,7,34],delimied_set:22,delimit:[4,7,19,20,21,22,23,24,25,26,33,34,36],delimited_set:[22,24,34],demograph:21,depend:[5,13,14,33,34],deprec:[7,34],deriv:[4,10,18,21,34,36],derivc:9,derive_categor:34,derot:[4,34,35],desc:[4,33],descend:4,describ:[0,2,3,4,7,14,19,21,28,31,34],descript:[4,5,7,9,10,19,27],descrp:4,design:[4,20],desir:[4,6,22,34,36],detail:[8,18,19],detect:[4,6,33],determin:[4,5,7],detractor:31,deutsch:[19,23],dff:20,diagram:28,dicat:24,dice:18,dichot:20,dichotom:[4,19,20,34],dict:[2,3,4,5,6,7,8,9,10,19,23,24,26,31,34,37],dictat:21,dictionari:[4,6,7,8,9,34],differ:[4,5,7,8,10,13,19,20,23,24,28,31,33,34,35,37],digest:19,dim:[5,10,31],dim_comp:34,dimens:[0,4,5,10,18,34,35],dimension:[4,34],dimensionizing_mapp:4,dimensions_comp:[4,20,34,38],dimlabel:34,dinner:22,direct:26,directli:[2,4,22,34],discret:[7,13],disk:[4,21,34],dispers:5,distinct:[19,31],distribut:[5,10],div:[7,34],dive:[19,24,38],divid:10,dk_filter:4,dms:4,dmsrun:4,doc:[22,34],docstr:6,document:[0,4,7,19,20,23,34],doe:[4,11,23,26,34],doesn:[4,25,26],dog:19,don:[19,22,24,36],done:[4,13,14],doubl:22,down:38,downbreak:[28,31,34],download:38,draft:34,draw:[21,24],drawn:[4,34],drink:37,drink_1:37,drink_2:37,drink_level:37,driven:0,drop:[4,5,7,10,21,34],drop_cod:[7,34],drop_delimit:4,drop_dupl:4,drope:[4,37],dropna:[4,6,34,37],dropx:4,dto:38,dtype:[20,22,26,34],due:34,dump:20,dupe:33,duplic:[4,23,33,34],durat:22,dure:[4,7,10,24],each:[2,4,5,6,7,13,14,19,23,24,28,31,33,34,37,38],eagleston:0,eas:34,easi:[0,19,22,23,34],easier:[12,22,28,34],easiest:22,easili:[4,12,20,31,34],ebas:[7,34],echo:4,ect:34,edit:[0,4,8,10,13,14,15,18,19,21,24,26,34],edit_param:8,eff:10,effect:[6,10,26,34],effici:34,ein:[19,23],either:[4,5,10,13,19,21,22,23,26,34],element:[4,7,10,19,34,38],eleph:4,els:26,emploi:19,empti:[4,7,22,24,26,33,34],empty_item:[4,34],en_u:7,enabl:[23,34],encod:[4,33,34],encount:20,end:[2,4,7,11,12,33,34],end_tim:22,enforc:34,eng:34,engin:[1,27,33],english:[19,34],enhanc:34,enough:4,enrich:34,ensur:[4,11,34],enter:[4,14],entir:[4,10,20,34],entri:[4,6,7,10,19,26,34],enumer:[4,7,23,24,26,33,36],environ:4,eponym:34,eqaul:4,equal:[4,10,34],equip:31,equival:[4,38],eras:[4,7],error:[4,10,33,34],escap:4,especi:[22,26,31],estim:10,etc:[4,19,21,28,31,34],ethnic:[13,22,28,31],even:[4,31,34],ever:20,everi:[22,23,26],everyon:26,everyth:26,evid:31,exact:[4,34],exactli:[4,24,25],exampl:[2,4,5,7,11,18,19,20,22,24,25,28,31,34,37,38],excel:[0,3,4,5,34],except:[4,21,34],exchang:[10,23],exclud:[4,5,7,8,10,31,34],exclus:[4,25,34],execut:4,exercis:[22,24],exist:[4,7,10,12,13,18,21,23,24,26,31,33,34],expand:[4,7,8,10,31,33],expect:24,experiment:4,explain:14,explicit:26,explicitli:[4,7,34,36],explor:19,expos:23,express:[4,5,6,7,10,22,25,34],ext_item:[4,34],ext_valu:4,ext_xk:34,ext_yk:34,extend:[4,5,7,13,18,22,31,34],extend_cod:[7,34],extend_filt:14,extend_i:[13,34],extend_item:[4,34],extend_valid_tk:34,extend_valu:[4,23,34,36],extend_x:[33,34],extens:[3,4,7,19,20,34],extra:34,extract:14,extrapol:25,factor:[4,6,7,34],factor_label:[7,34],factormap:4,fail:[4,34],failur:20,fall:[4,5,10],fallback:8,fals:[4,5,7,10,13,20,22,23,24,26,31,34],fast_stack_filt:34,favour:10,featur:[19,34],feed:[4,34],feedback:4,femal:[4,22],few:[11,21,22,34,37],figur:10,file:[2,3,4,5,7,11,20,21,22,28,34,38],file_nam:20,filenam:[2,7],fill:[4,18,34],fillna:[4,18],filter1:34,filter2:34,filter:[4,6,7,10,11,15,18,28,31,33,34],filter_1:34,filter_2:34,filter_def:6,filter_kei:[7,14],filter_nam:34,find:[4,11,22,28,34],find_duplicate_text:4,finish:[4,21,34],finnish:19,first:[4,5,10,13,19,21,26,31,33,34],fit:[19,23,24],fix:[4,15,33,34],flag:[4,5,34],flag_bas:10,flat:4,flatten:[4,24,34],flexibl:[6,7,24],float64:20,folder:20,follow:[4,8,10,11,14,15,19,20,21,22,23,24,26,28,31,34,37,38],folow:26,footbal:24,forc:[26,34],force_text:[4,23,34],forcefulli:4,form:[3,4,7,10,14,20,23,24,25,26,28,34],format:[0,4,6,10,19,20,23,26,31,34,37],former:[4,7,22,23,24,34],fortnight:22,found:[2,3,4,7,10,20,22,24,26,31,33,34],four:[28,31],frang:[4,25,26,34,37],freez:7,freeze_master_meta:7,french:19,frequenc:[4,5,8,9,10,28,31,34],freysson:0,from:[0,2,3,4,5,7,10,12,14,15,18,19,21,24,25,26,28,31,33,34,37],from_batch:[4,34],from_compon:[4,20,34],from_dichotom:[4,34],from_excel:[4,34],from_sav:7,from_set:[4,20,34],from_stack:[4,34],front:34,fulfil:4,full:[3,4,5,7,21,23,25,34,38],fullnam:31,fully_hidden_arrai:4,fun:34,further:21,futur:[7,34],geir:0,gender:[4,13,14,15,22,24,26,28,31,34,37],gener:[2,4,5,7,8,9,11,20,22,23,26,31,34],generate_report:34,german:[19,23],get:[4,7,8,11,12,13,14,22,28,31,34,36],get_batch:[4,12,13,28,31,33],get_edit_param:8,get_properti:4,get_qp_dataset:34,get_s:10,get_sig:10,get_statist:10,get_std_param:8,getter:34,give:[13,26,34],given:[4,5,6,7,10,20,26,34],global:[4,8,10,14,15,21,34],goe:4,going:36,grab:33,greater:4,grid:[4,34],griffith:0,group:[4,5,6,7,8,10,19,21,22,24,25,28,31,33,34],group_nam:6,group_target:6,grouped_ag:24,grp:10,grp_text_map:31,guid:34,gzip:7,hack:4,had:26,hand:24,handl:[0,6,7,9,10,20,31,34,36,38],handler:34,happen:[4,24],happend:4,has:[2,4,7,12,15,19,21,25,26,31,33,34,37],has_al:[4,5,18],has_ani:[4,18],has_count:18,has_other_sourc:8,have:[4,7,10,13,20,23,24,25,26,28,33,34,37],head:[20,22,23,24,26,34,38],heirarch:4,hell:4,hello:20,help:[26,28,31],helper:[4,31,34],here:[4,7,26,28,33,34,38],hidden:[4,33,34],hide:[4,15,33,34],hide_empti:34,hide_empty_item:[4,34],hide_on_i:4,hide_valu:[4,34],high:[5,10],higher:10,hmerg:[4,33,34],hockei:24,hold:[2,4,7,10,34],horizont:[4,18],household:20,how:[3,4,7,14,19,22,24,28,31,36,38],howev:[23,24,26,36,38],hrafn:0,html:[4,23,34],hub:34,ident:[4,20,24,34],identif:4,identifi:[4,5,23,34,37],ids:4,ignor:[4,7,10,22,24,34],ignore_arrai:4,ignore_cod:4,ignore_flag:10,ignore_item:[4,24,34],ignore_valu:[4,24],ill:23,implement:[7,10,19,22,34],impli:4,implicitli:10,impract:28,impute_method:6,incl:[7,34],includ:[2,3,4,5,7,10,13,25,26,28,31,33,34,37],inclus:[24,34],incom:[4,10,24],inconsist:[4,15,23,33,34],incorrect:34,incorrectli:34,independ:[4,10],index:[0,2,4,7,10,34],indic:[4,5,7,10,23,24,26,34],individu:[3,4,34],industri:20,infer:[7,20,24,34],info:[4,18,19],inform:[0,4,8,9,13,14,15,19,20,23,26,31,34,36,37],inherit:[11,34],inhomogen:10,init:26,initi:[4,18,34,37],inject:[4,26],innermost:8,inplac:[4,7,10,18,21,26,34],input:[4,6,7,10,11,20,34],insert:[4,13,34],insid:[2,4,9,10,19,20,22,23,34],inspect:[4,18,34],instal:[4,34],instanc:[2,3,4,7,9,10,11,14,20,21,22,23,24,28,31,34],instead:[4,7,10,20,25,26,34],instruct:[5,10,19,26,31,34],int64:[20,22],integ:10,integr:22,intend:7,inter:[4,6],interact:[2,3,4,21,22,34],interfac:0,interim:34,interlock:[4,18,34],intern:[2,34],interpret:[4,25],intersect:[4,18,22,34],intro:21,introduc:10,involv:26,iolocal:[7,20],ioutf8:7,ipython:[4,21,34],is_arrai:[31,34],is_bas:8,is_block:31,is_count:8,is_cumul:8,is_dat:34,is_delimited_set:34,is_float:34,is_g:[4,25,34],is_int:34,is_like_numer:[4,22,24,34],is_meanstest:8,is_multi:31,is_nan:[4,22,25],is_nest:31,is_net:8,is_pct:8,is_propstest:8,is_singl:34,is_stat:8,is_str:34,is_sum:8,is_weight:[8,31],isol:34,issu:[20,33,34],ist:[19,23],item:[4,10,11,13,19,20,21,22,23,24,31,34,36,37,38],item_nam:[4,34],item_no:[4,34],item_text:[4,22,34],iter:[9,22,34],its:[2,4,5,7,8,10,11,12,13,14,15,19,20,22,24,25,26,31,34,37,38],itself:[4,10],jame:0,jjda:20,jog:24,join:[4,22],json:[4,7,11,18,34],jupyt:[4,21,34],just:34,keep:[4,9,10,21,23,24,34,37],keep_bas:10,keep_cod:[10,34],keep_origin:4,keep_variable_text:[4,34],kei:[2,4,5,7,8,11,14,19,23,24,26,31,34,37],kept:[4,10,11,34],kerstin:0,keyword:[5,9,34],kind:[22,24,28,34],kite:[19,24,38],know:[14,19,22,24,36,38],kritik:31,kwarg:[4,5,8,9,34],lab:7,label:[4,5,7,14,19,20,22,23,24,26,34,36,37,38],lack:0,lang:19,languag:[11,18,23,34],larg:[14,21,28,31,34],last:4,lastli:34,later:[4,13,20,34],latest:[0,32,34],latter:[22,34],lead:[24,31,34],least:[4,22,23],leav:[23,24],left:[4,10,33,34],left_id:4,left_on:[4,34],legaci:4,lemonad:37,length:[4,34],less:[22,33,37],let:[23,24,28],level:[4,5,7,10,14,19,26,31,34,37],lib:[4,19,34,38],librari:[0,19,34],lift:24,like:[0,4,5,7,13,14,19,21,22,24,26,28,31,34,37,38],limit:[4,10,24,26,34],link:[2,4,5,7,8,9,10,11,14,27,31,34],list:[2,4,5,6,7,9,10,13,18,19,21,22,23,24,26,33,34,36,37],list_vari:34,listen:26,load:[2,3,4,7,11,34],load_cach:7,loc:31,local:[7,13,22,28,31],locat:[2,4,7,34],logic1:34,logic2:34,logic:[4,5,8,10,18,22,24,26,34],logic_a:[25,26],logic_b:[25,26],logic_c:[25,26],london:4,longer:34,look:[4,13,24,26,34,37,38],loop:[34,37],lose:[13,24,37],lot:[4,21,22,26],low:[4,5,10,33],lower:[4,10,33,34],lower_q:[10,33],lunch:22,machin:[4,20,34],made:2,mai:[4,25,26],main:[4,6,10,13,14,19,24,31,34],main_filt:34,mainli:3,major:10,mak:34,make:[4,5,22,24,28,34],make_summari:[13,34],make_templ:9,male:[22,26],manag:[0,18,26],mani:[13,28],manifest:4,manipul:[10,13,15],manual:[4,5,34,36],map:[4,5,6,7,9,10,18,20,23,34,36],mapper:[4,18,25,34,37],mapper_to_meta:4,margin:[5,10],mark:10,market:[0,20],mask:[4,18,21,22,23,34,36,37,38],mass:5,massiv:26,master:34,master_meta:7,match:[4,6,7,20,24,34,38],matric:10,matrix:10,matrixcach:7,matter:[11,33],max:[7,10,31,33,34],max_iter:6,mdd:[0,4,20,34],mdm_lang:34,mean:[4,5,6,7,8,10,13,15,20,22,24,25,26,31,33,34,37],measur:[5,10],median:[6,7,10,31,33,34],membership:[22,34],memori:[21,34],memoryerror:34,men:[4,14,15,28,31],mention:[12,23],merg:[3,4,18,33,34],merge_exist:[4,33],merge_text:[4,34],messag:34,meta:[4,5,7,8,11,12,14,15,18,20,21,22,24,26,31,33,34,36,37,38],meta_dict:[4,34],meta_edit:[4,7,15],meta_to_json:[4,34],metadata:[0,4,7,18,19,20,24,26,34,36,38],metaobject:34,method:[2,3,4,5,6,7,8,9,10,11,12,13,14,18,20,21,22,23,24,31,33,34,36,37,38],metric:[5,7,10],mid:[5,10,22,34],middl:34,might:[7,21,23,24,34],mimic:[5,10],mimick:[5,10,22],min:[4,7,10,31,33,34],min_value_count:[4,33],minimum:5,minor:34,mismatch:34,miss:[4,6,8,14,20,22,23,24,33,36,38],missing_map:[4,34],mix:[4,24],mode:[6,7,20,34,35],modifi:[4,7,10,15,24,34],modu:34,modul:[0,4,7,33],month:22,more:[2,4,22,25,26,31,33,34],morn:22,most:[10,21,22,26,33,34],mous:4,move:[0,4,21,34],mrs:4,mrset:4,mrset_tag_styl:4,much:[28,37],mul:7,multi:[5,10,19],multiindex:10,multipl:[0,3,4,5,19,22,23,25,26,34],multipli:10,multivari:10,must:[4,6,7,20,21,23,26,31,34,37],name:[2,3,4,5,6,7,8,9,10,12,13,19,20,21,22,23,24,26,28,31,33,34,36,37,38],name_data:[34,38],nan:[4,10,20,22,23,24,26,28,31,34,38],nate:4,nativ:[0,4,18,34],natur:[10,21,22],necessari:[10,21,34,37],need:[4,7,9,10,21,23,24,34,38],neg:34,nest:[7,8,19,25,26],net:[5,7,8,10,18,27,33,34],net_1:[7,31,33],net_2:[7,31,33],net_3:7,net_def:33,net_map:[7,31],net_view:34,never:[22,23],new_arrai:23,new_array_1:23,new_array_2:23,new_array_3:23,new_array_4:23,new_array_97:23,new_array_98:23,new_chain:34,new_cod:10,new_column:4,new_d:34,new_data:7,new_data_kei:7,new_dataset:4,new_int:23,new_meta:7,new_nam:[4,23,24,34],new_ord:[4,21,34],new_rul:34,new_set:[4,34],new_singl:23,new_stack:2,new_text:[4,23],new_var:34,new_weight:7,newli:34,next:[4,7,11,19,31,34,38],no_data:24,no_filt:[4,7,28,31,34],non:[4,7,22,25,27,34],none:[2,3,4,5,6,7,8,9,10,13,20,21,22,23,24,26,28,33,34,36,38],none_band:34,nonea:4,normal:[4,5,7,10,33,34],norwegian:19,not_al:18,not_ani:[4,10,18,34],not_count:18,notat:[5,7,8,10],note:[2,4,5,26,31],notebook:[4,21,34],notimplementederror:[15,34],now:[13,14,21,31,34,37],num:5,number:[4,6,10,20,22,23,24,33,34],numer:[4,5,7,18,19,23,24,31,34,36],numpi:[0,10,34],obei:22,object:[2,3,4,5,7,10,18,20,21,24,26,28,31,34,36],obscur:26,observ:0,obvious:31,occur:26,oe_q8:13,oe_q9:13,offer:[0,4,19,22,24,34,36,38],often:[21,22,24],old:[4,5,7,34],old_cod:10,old_nam:34,older:34,omit:26,omnibu:34,on_var:[7,31],onc:22,one:[2,4,5,12,13,19,21,22,23,24,26,28,31,33,34],ones:[4,10,15,21,22,34],onli:[4,7,9,10,11,13,14,21,23,24,26,28,31,33,34,36,37],only_men:24,only_typ:[4,7],onto:26,oom:34,open:[0,11,12,34],oper:[4,5,20,22,24,25,26,34],operat:18,opportun:20,oppos:21,opt:4,option:[4,5,6,7,10,13,14,19,22,23,31,34],order:[2,4,11,18,19,24,34],ordereddict:[3,4,7,13,28,31],organ:16,orgin:10,orient:[2,34],origi:4,origin:[4,7,10,21,24,34],other:[4,5,7,10,11,12,13,19,23,24,25,26,33,34,37],other_sourc:[7,31,34],otherwis:[4,34],our:[0,22,26],out:[5,12,14,24,31,34],outcom:4,outdat:34,output:[4,7,10,13,22,34],outsid:34,over:[4,7,9,10,34],overcod:[4,7,31],overlap:10,overview:[4,7,22,28,34],overwrit:[4,15,21,26,34],overwrite_margin:10,overwrite_text:4,overwritten:[4,7,34],ovlp_correc:10,own:[13,34],pack:4,packag:5,paint:[3,34],painter:34,pair:[4,5,10,18,23,26,34,36],panda:[0,3,4,6,7,10,19,20,22,26,34],pane:4,parachut:[19,24,38],parallel:7,paramet:[3,4,5,6,7,8,9,10,13,21,22,23,24,26,31,33,34,37],parent:[4,19,20,34],pars:5,part:[4,8,19,21,22,26,34],parti:18,particip:20,particular:4,pass:[4,5,7,10,21,22,24,34],past:26,path:[2,3,4,7,20,34],path_clust:3,path_csv:20,path_data:[4,38],path_ddf:[4,20,34],path_json:20,path_mdd:[4,20,34],path_meta:4,path_report:4,path_sav:[4,20],path_sav_analysi:20,path_stack:7,path_txt:20,path_xlsx:[4,34],path_xml:20,pct:4,peopl:20,per:[4,5,6,7,10,12,20,22,23,24,26,34,38],percentag:[7,8,10,31,34],perform:[4,5,7,8,10,21,26,31,33,34],perman:21,physic:34,pick:[4,24],pickl:2,pilat:24,pivot:7,place:[10,26,34],plai:22,plain:[0,8,20],plan:[11,13,14,19,24,28,31],pleas:[7,21,22,34],point:19,pointer:19,pool:10,popul:[4,11,13,14,27,31,34],portion:7,posit:[4,10,21,22,23,28,34],possibl:[3,4,5,7,12,13,19,22,23,25,31,33,34,37],power:0,powerpoint:[5,34],powerpointpaint:34,pptx:34,pre:[4,26,31],precis:[26,34],prefer:22,prefix:[4,7],prep:25,prepar:[3,21,23,31,34],present:[4,10,34],preset:[4,7],pretti:[4,26],prevent:[4,15,21,23,24,33,34],previou:[21,34],previous:[4,34],primarili:4,print:[13,22,28,31,34],prior:[4,24],prioriti:34,probabl:[19,24,34],problem:[33,34],process:[0,4,7,9,20,21,22],produc:[5,10,13,24],product:[34,37],profession:[4,34],progress:[4,34],prohibit:23,project:0,promot:31,promotor:31,prop:[5,31],prop_nam:4,prop_valu:4,proper:[34,38],properli:38,properti:[4,10,11,20,34],proport:[5,6,8],protect:4,provid:[3,4,5,7,8,9,10,19,20,21,22,23,24,26,31,34,36],proxi:7,purpos:19,put:7,python:[4,33],q01_1:4,q01_3:4,q11:34,q11_grid:34,q12:37,q12_10:37,q12_11:37,q12_12:37,q12_13:37,q12_1:37,q12_2:37,q12_3:37,q12_4:37,q12_5:37,q12_6:37,q12_7:37,q12_8:37,q12_9:37,q12_:37,q12a:37,q12a_10:37,q12a_11:37,q12a_12:37,q12a_13:37,q12a_1:37,q12a_2:37,q12a_3:37,q12a_4:37,q12a_5:37,q12a_6:37,q12a_7:37,q12a_8:37,q12a_9:37,q12a_grid:37,q12b:37,q12b_10:37,q12b_11:37,q12b_12:37,q12b_13:37,q12b_1:37,q12b_2:37,q12b_3:37,q12b_4:37,q12b_5:37,q12b_6:37,q12b_7:37,q12b_8:37,q12b_9:37,q12b_grid:37,q12c:37,q12c_10:37,q12c_11:37,q12c_12:37,q12c_13:37,q12c_1:37,q12c_2:37,q12c_3:37,q12c_4:37,q12c_5:37,q12c_6:37,q12c_7:37,q12c_8:37,q12c_9:37,q12c_grid:37,q12d:37,q12d_10:37,q12d_11:37,q12d_12:37,q12d_13:37,q12d_1:37,q12d_2:37,q12d_3:37,q12d_4:37,q12d_5:37,q12d_6:37,q12d_7:37,q12d_8:37,q12d_9:37,q12d_grid:37,q14_1:4,q14_1_1:4,q14_1_2:4,q14_1_3:4,q14_2:4,q14_2_1:4,q14_2_2:4,q14_2_3:4,q14_3:4,q14_3_1:4,q14_3_2:4,q14_3_3:4,q1_1:[4,25,26,37],q1_2:[4,26,37],q1_3:[4,26],q1_rec:4,q2_count:22,q2array_tran:4,q2b:[13,22,28,31],q3_no_data:24,q3_only_men:24,q3_rec:24,q3_version2:24,q4a:34,q4a_1:34,q4a_2:34,q4a_3:34,q4a_grid:34,q5_1:[19,21,22,24,34,38],q5_2:[19,21,22,24,34,38],q5_3:[19,21,22,24,34,38],q5_4:[19,21,22,24,34,38],q5_5:[19,21,22,24,34,38],q5_6:[19,21,22,24,34,38],q5_grid:38,q5_tran:24,q5_trans_1:24,q5_trans_2:24,q5_trans_3:24,q5_trans_4:24,q5_trans_5:24,q5_trans_97:24,q5_trans_98:24,q6_1:[13,21,22,28,31],q6_2:[13,21,22,28,31],q6_3:[13,21,22,28,31],q6_calc:31,q6_grid:38,q6_net:31,q6copi:38,q6new:38,q6new_grid:38,q6new_q6copi:38,q6new_q6copy_grid:38,q6new_q6copy_tran:38,q6new_q6copy_trans_grid:38,q7_1:[21,22,34],q7_2:[21,22,34],q7_3:[21,22,34],q7_4:[21,22,34],q7_5:[21,22,34],q7_6:[21,22,34],q7_grid:38,q8_with_a_new_nam:23,q8a:[13,22],q9a:[13,22],q_group:37,q_label:[4,34],qtp:37,qtype:[4,23,33,36,38],qualifi:[4,5,10],quantifi:1,quantipi:[2,3,4,5,6,7,8,9,10,11,19,20,22,25,31,34,36,38],quantipyview:[1,7,34],quantiti:[10,34],queri:[2,4,6,7,18,19,24,34,38],question:[4,10,19,24,26,31,34,37],questionnair:21,quick:[4,22,34],quickli:[6,21,22,24,34],radio:26,radio_st:26,radio_stations_cb:26,radio_stations_xb:26,rais:[4,15,21,31,33,34],rake:34,rang:[4,5,18,21,34],rate:[34,37],raw:[5,10],raw_sum:10,rbase:7,read:[0,4,20,34],read_ascrib:[4,20],read_deciph:20,read_dimens:[4,20],read_quantipi:[4,11,20,34,38],read_spss:[4,20],rebas:10,rebuild:24,rec:[4,34],receiv:37,recod:[0,4,7,18,34],recode_from_net_def:7,recode_seri:4,recoded_filt:34,recommend:24,record_numb:[13,22],reduc:[4,7,10,21,34],reduced_d:21,reduct:4,refactor:34,refer:[4,7,10,19,23,26,28,31,34],referenc:[7,13,19,26,34],reflect:[4,10,21,34],refresh:7,refus:[19,24],regard:[4,34],region:[4,34],regist:[4,22,34],regroup:[4,34],regular:[4,19,31,34],regularli:[22,23,24],reindex:4,reintroduc:34,rel:21,rel_to:8,relat:[2,8,23,26,34],relation_str:8,relationship:7,relev:[23,34],religion:22,reload:[21,34],remain:[4,5,21,26,34],rememb:36,remind:36,remov:[4,5,7,10,12,13,18,34],remove_data:7,remove_filt:34,remove_html:[4,34],remove_item:4,remove_valu:[4,33],renam:[4,18,24,33,34,38],rename_from_mapp:4,renamed_item:4,renamed_v:4,reorder:[18,34],reorder_item:[4,34],reorder_valu:4,reorgan:0,repair:[4,33,34],repair_text_edit:[4,34],repeat:[21,28],repetit:26,replac:[4,10,13,23,26,31,34],replace_i:[13,34],replace_text:[4,34],report:[0,4,5,6,34],reposit:[4,21,34],repres:[4,26],represent:[7,8,10,19,24,34],request:[5,7,13,21,23,26,31,34],request_view:34,requir:[4,21,23,34,38],rescal:[5,7,8,10,31],research:[0,20],reset:[4,34],reset_index:4,resid:34,resolv:34,resolve_nam:[4,34],resp:34,respect:[4,10,24,34,36],respond:[10,21,37],respons:[19,22,25,26,34,37],responsess:34,restor:[4,7,21,34],restore_item_text:4,restore_meta:7,restrict:[4,5,7,10,19,21,22,34],result:[3,4,5,7,9,10,16,20,22,23,24,26,28,34],result_onli:10,retain:9,retriev:10,revers:[24,25],revert:[4,21,34],rewrit:33,right:[4,33,34],right_id:4,right_on:[4,34],rim:[1,4,34],roll:4,roll_up:4,rollback:[18,34],rolled_up:4,round:6,row:[4,5,10,18,20,22,34,38],row_id:4,row_id_nam:4,rule:[4,33,34,38],run:[4,7,10,15,24,28,31,34],safe:[4,23],safeguard:4,sai:26,same:[3,4,7,13,19,20,22,26,28,33,34,37,38],sampl:[5,8,10,33,34],sample_s:14,sandbox:34,satisfi:34,sav:[4,7,20],save:[2,3,4,7,21,28,34],savepoint:18,scalar:34,scale:[5,6,10,19,34,37],scan:4,scenario:38,scheme:[4,6,19,34],scratch:[18,34],script:4,search:4,second:[4,5,10,15,31],sect:4,section:[8,10,11,14,21,26],see:[13,21,24,26,28,31,34],seen:[26,38],segemen:26,segment:18,select:[4,7,9,13,14,21,22,31,34,37],select_text_kei:4,self:[2,4,7,10,26,28,34],sem:[7,10,33],semi:34,sensit:[4,33,34],separ:[4,26,36],septemb:[],sequenc:4,seri:[4,19,22,26,34],serial:2,session:[21,34],set:[3,4,5,6,7,9,10,11,12,13,18,19,20,22,23,24,26,28,33,34,36],set_cell_item:14,set_col_text_edit:34,set_column_text:34,set_dim_suffix:34,set_encod:4,set_factor:4,set_item_text:[4,23,34],set_languag:14,set_mask_text:34,set_miss:[4,34],set_opt:34,set_param:10,set_properti:[4,15],set_sigtest:[14,31,34],set_target:6,set_text_kei:4,set_unwgt_count:34,set_val_text_text:34,set_value_text:[4,15,23,34],set_variable_text:[4,15,23,34],set_verbose_errmsg:4,set_verbose_infomsg:4,set_weight:14,setdefaultencod:4,setnam:[4,34],setup:[4,10,13,34],sever:[5,22,37],shape:[4,21,31],share:[4,19],sheet:[4,34],shop:4,short_item_text:34,shorten:[4,34],shorthand:[4,5],shortnam:[5,8],should:[3,4,7,14,21,22,26,34,37,38],show:[4,10,13,19,21,22,31,34],shown:[4,10,33,34],side:[9,34],sig:[5,7,10,14,34],siglevel:34,signific:[5,10,11,27],significancetest:34,sigproperti:34,similar:[28,37],similarli:[22,23],similiar:23,simpl:[5,6,10,19,25,34,36],simpli:[4,22,23,24,25,31,34],simplifi:[24,26],sinc:[10,26,31,38],singl:[3,4,7,19,20,21,22,23,24,26,33,34,36,38],sit:26,six:22,size:[5,8,10,21,22,31,34],skip:[22,23,31,34],skip_item:34,slice:[4,8,15,18,34],slicer:[4,18,22,24,34],slicex:4,small:[5,34],snack:22,snapshot:[4,21,34],snowboard:[19,24,38],soccer:24,social:0,softwar:[0,4,5,10,20,33],solut:34,solv:34,some:[13,14,15,22,25,26,33,34,37],someth:37,sometim:[21,28,34],soon:34,sorri:0,sort:[4,15,33,34],sort_by_weight:[4,33],sort_on:33,sortx:4,sourc:[0,4,7,19,20,22,23,34,38],source_item:4,space:[4,25],speak:19,spec:3,spec_condit:8,specfic:14,special:[0,11,14,19,28,31,34],specif:[3,4,5,7,8,10,11,13,14,15,19,21,23,34,38],specifi:[2,4,5,6,7,10,13,20,23,31,34,36],speed:34,spell:[4,34],split:[4,7,13,34],split_view_nam:7,sport:[20,22],spreadsheet:0,spss:[0,4,10,18,34],spss_limit:[4,34],squar:7,stack:[1,2,3,4,5,8,11,13,14,27,31,34],stage:[4,34],standalon:18,standard:[8,10,20,34],standardli:24,start:[4,18,23,24,26],start_meta:[4,34],start_tim:22,stat:[4,5,7,10,31,34],state:[4,15,18,24,34],statement:[4,5,19,25,26],statisfi:34,statist:[0,4,5,7,8,10,18,27,28,34],std_paramet:8,stddev:[7,10,31,33,34],ste:34,stem:34,step:[31,36],still:[26,34],store:[4,5,7,11,12,13,19,21,24,28,31,34],store_cach:7,str:[3,4,5,6,7,8,9,10,24,34,36,37],str_tag:[4,34],strict:[4,34],strict_select:9,strictli:23,string:[2,4,6,7,8,10,19,20,21,22,23,24,25,34],strip:34,structur:[0,4,6,7,9,11,13,19,20,21,24,28,34,37],studi:34,style:[4,7],sub:[7,31],subclass:[2,11],subclasss:15,sublist:34,subset:[4,9,18,22,24,34],subset_d:4,substr:[4,34],subtl:34,subtyp:[19,34,38],suffix:[4,5,24,34,38],sum:[4,5,7,8,10,27,34],summar:[4,5,10,34],summari:[4,5,6,7,8,10,13,22,31,33,34],summaris:7,summat:10,suppli:24,supporintg:7,support:[0,7,8,18,19,22,23,24,33,34,38],surf:[19,24,38],survei:21,sv_se:[31,34],swap:[7,8,10,34],swedish:[19,34],swim:24,syntax:34,sys:4,tab:20,tabl:[0,7],tabul:[5,13,28],tag:[4,23,34],take:[4,5,7,11,22,24,25,26,34,37],taken:[4,7,14,15,24,33,34],target:[4,6,18,23,33,34],target_item:4,task:22,team:22,temp:4,templat:[5,9,34],temporari:[4,34],temporarili:4,ten:26,tend:4,term:[7,23,34],termin:34,test:[2,4,5,8,10,11,22,27,34,36],test_cat_1:36,test_cat_2:36,test_cat_3:36,test_tot:[5,10,34],test_var:[34,36],testtyp:10,text1:20,text:[4,5,7,8,14,18,20,21,22,24,26,31,34,36,38],text_kei:[3,4,7,11,18,22,23,31,34],text_label:[4,34],text_prefix:7,textkei:[4,34],than:[4,22,24,33,34],thei:[4,10,13,14,20,25,26,31,33,34],them:[4,5,13,20,22,26,31,33,34],themselv:[4,10],therefor:[4,5,24,34],thi:[2,3,4,5,6,7,10,13,14,15,20,21,22,23,24,26,28,31,34,37,38],third:18,thorugh:24,those:4,three:[4,21,22,24,26,34,36],threshold:5,through:[2,3,4,9],throughout:[4,19,20,34],thu:6,time:[7,19,21,22],titl:13,tks:34,to_arrai:[4,33,34,37],to_delimited_set:[4,34],to_df:10,to_excel:7,todo:[4,5,6,7,9,10],togeth:[3,4,7,19,21],toggl:7,too:34,tool:[5,20,24,25],top2:31,top3:7,top:26,topic:[19,34],total:[4,5,6,10,13,34],toward:34,tracker:34,tradit:10,transfer:34,transform:[0,4,5,10,18,34],translat:23,transpos:[4,13,24,31,38],transpose_arrai:13,transposit:24,treat:[4,10,25,31],tree:28,treshhold:10,trigger:4,tstat:10,tupl:[4,8,23,24,34,36],turn:19,two:[4,5,10,13,19,21,23,28,31,33,34],txt:[4,20],type:[0,3,4,5,6,7,8,9,10,13,18,20,23,26,31,33,34,36,38],type_nam:7,typic:26,ultim:4,unabbrevi:25,unattend:4,unbind:4,uncod:[4,34],uncode_seri:4,uncodit:5,unconditi:10,under:[4,5,34],underli:19,understood:20,undimension:4,undimensionizing_mapp:4,undo:7,uni:[5,10],unicod:7,unifi:[4,34],uniformli:20,unify_valu:4,union:[18,26],uniqu:[4,5,7,24,28,31,34],unique_id:[4,22],unique_kei:[4,34,37],uniquify_kei:4,unkei:26,unless:4,unlik:[19,24],unpool:10,unqiu:24,unrol:[4,31,34],untouch:[23,33],unusu:34,unwant:[4,34],unweight:[7,10,31,33,34],unweighted_bas:[7,31,34],unwgt:34,upcom:[],updat:[4,8,9,10,23,33,34],update_axis_def:[10,34],update_exist:[4,34],upon:19,upper:[4,34],upper_q:[10,33],uppercas:33,usag:[23,33,34],use:[0,2,4,5,7,10,12,13,19,20,21,22,23,24,26,34,35],use_ebas:10,used:[2,3,4,5,7,8,9,10,11,14,15,20,21,24,26,31,33,34],useful:[21,22,34],user:[2,4,14,34,36],userwarn:[34,36],uses:[4,10,34],using:[0,2,3,4,6,7,19,20,21,24,25,26,28,31,33,34,38],usual:19,utf8:33,utf:7,val:4,val_text_sep:4,valid:[4,6,7,14,19,24,26,31,34,36],valid_cod:34,valid_tk:[11,34],valu:[3,4,5,6,7,8,9,10,18,20,21,24,25,26,31,33,34,35,38],value_count:[4,22,34],value_map:37,value_text:[4,22,34],valueerror:[4,21,23,34],var_exist:[22,34],var_grid:37,var_nam:[34,37],var_suffix:4,varcoeff:10,vari:22,variabl:[0,4,5,6,7,8,10,11,18,19,20,23,27,33,34,36,37,38],variable_typ:7,variables_from_set:34,varianc:10,variant:[22,34],varibal:37,varibl:34,variou:[5,14,22,28,31],varlist:[4,34],varnam:[4,34],vector:10,verbatim:[11,34],verbos:[4,7,25,31,34],veri:[19,23,24,34],versa:10,version2:24,version:[4,5,7,10,19,21,23,24,26,34],versu:31,vertic:[4,18],via:[0,4,5,7,21,22,23,24,31,34],vice:10,view:[1,2,3,4,5,7,9,10,14,16,22,27,28,33,34],view_kei:31,view_name_not:10,viewmanag:34,viewmapp:[1,7],viewmeta:8,visibl:[31,34],vmerg:[4,33,34],wai:[7,12,13,19,21,22,23,26,31,34,35],wait:21,want:[21,24,26],warn:[4,31,33,34],water:37,wave:21,weak:[4,33],weak_dup:4,week:22,weight:[0,4,6,7,8,10,11,12,22,24,31,33,34],weight_a:[14,22,31],weight_b:22,weight_column_nam:6,weight_nam:4,weight_schem:4,weigth:4,well:[4,10,20,22,25,26,31,34,38],went:34,were:[26,33,34],wgt:34,what:[16,19,20,24,26,27,34,35,38],whatev:[4,26],when:[4,5,7,10,20,21,23,24,26,34,38],where:[2,3,4,10,24,25,26],whether:[5,10],which:[4,5,7,10,11,13,14,15,22,23,24,26,28,31,33,34,37],whole:[4,34],whose:[4,7,34],wide:34,wil:4,wildcard:26,window:20,windsurf:[19,24,38],wise:[4,31],witch:34,within:[4,10,33],without:[33,34],women:15,work:[4,11,21,23,31,34],workbook:3,workspac:34,world:20,would:[4,19,24,26,34],wouldn:[19,24],wrap:34,wrapper:[4,10,33],write:[4,7,20,21,34,38],write_dimens:[4,33,34],write_quantipi:[4,21],write_spss:[4,20],writen:37,written:[21,34],wrong:34,x_filter_map:[28,31],x_kei:14,x_y_map:[13,14,28,31],xdef:10,xks:[4,7,31,34],xlsx:4,xml:[4,20],xsect:10,xtotal:4,y_filter:34,y_kei:[14,28,31],y_on_i:[13,28,31,34],year:[19,22,38],yes:20,yet:33,yield:22,yks:[4,34],yoga:24,you:[4,11,13,14,19,20,21,22,23,24,26,28,31,34,37,38],younger:24,your:[4,19,20,21,24,26,34],ysect:10},titles:["Quantipy: Python survey data toolkit","API references","Chain","Cluster","DataSet","QuantipyViews","Rim","Stack","View","ViewMapper","quantify.engine","Batch","Creating/ Loading a qp.Batch instance","Adding variables to a qp.Batch instance","Set properties of a qp.Batch","Inherited qp.DataSet methods","Builds","Combining results","Data processing","DataSet components","I/O","DataSet management","Inspecting variables","Editing metadata","Transforming variables","Logic and set operaters","Custom data recoding","Analysis & aggregation","Collecting aggregations","The computational engine","Significance testing","View aggregation","Release notes","Latest (01/10/2018)","Archived release notes","How-to-snippets","Different ways of creating categorical values","Derotation","DataSet Dimensions compatibility"],titleterms:{"boolean":25,"case":[19,21,22],"default":26,Adding:[13,26],The:[19,26,29,38],Using:20,about:37,access:38,adding:23,aggreg:[13,17,27,28,31],analysi:27,api:1,append:26,archiv:34,arrai:[13,19,24,37,38],ascrib:20,band:[24,26],base:26,basic:31,batch:[11,12,13,14],build:[16,26],calcul:31,categor:[19,24,31,36],cell:14,chain:[2,17],chang:23,clone:21,cluster:3,code:[25,26],collect:28,column:[19,21],combin:17,compat:38,complex:25,complic:26,compon:[19,20],comput:29,condit:26,convers:[20,24],copi:24,creat:[12,17,23,26,36,38],creation:26,csv:20,cumul:31,custom:[17,26],data:[0,18,19,22,26,38],datafram:20,dataset:[4,15,19,21,23,37,38],deciph:20,definit:31,deriv:26,derot:37,descript:31,detail:26,dice:22,differ:36,dimens:[20,38],document:[],edit:23,end:13,engin:[10,29],exampl:26,exist:[22,25],extend:23,featur:0,fill:26,fillna:26,filter:[14,21],from:[20,23],has_al:25,has_ani:25,has_count:25,horizont:21,how:[35,37],info:23,inherit:15,initi:26,inplac:24,inspect:22,instanc:[12,13],interlock:26,intersect:[25,26],item:14,json:20,kei:[0,13],languag:[14,19],latest:33,link:28,list:25,load:12,logic:25,manag:21,map:19,mapper:26,mask:19,merg:21,meta:[19,23],metadata:[22,23],method:[15,26],mode:38,nativ:20,net:[26,31],non:31,not_al:25,not_ani:25,not_count:25,note:[32,34],numer:26,object:[19,22,23],open:13,operat:25,order:21,organ:17,pair:20,parti:20,popul:28,process:18,properti:14,python:0,quantifi:10,quantipi:0,quantipyview:5,queri:22,rang:25,recod:26,refer:1,releas:[32,34],remov:[23,26],renam:23,reorder:23,result:17,rim:6,rollback:21,row:21,savepoint:21,scratch:23,segment:26,septemb:[],set:[14,21,25],signific:[14,30,31],slice:22,slicer:25,snippet:35,special:13,spss:20,stack:[7,28],standalon:20,start:20,state:21,statist:[20,31],subset:21,sum:31,support:20,survei:0,target:26,test:[14,30,31],text:[19,23],text_kei:19,third:20,toolkit:0,transform:24,type:[19,22,24],union:25,upcom:[],use:37,valu:[19,22,23,36],variabl:[13,21,22,24,26,31],verbatim:13,vertic:21,view:[8,17,31],viewmapp:9,wai:36,weight:14,what:[17,28,37]}}) \ No newline at end of file diff --git a/docs/API/_build/html/sites/api_ref/00overview.html b/docs/API/_build/html/sites/api_ref/00overview.html index fd749a574..b74e5a520 100644 --- a/docs/API/_build/html/sites/api_ref/00overview.html +++ b/docs/API/_build/html/sites/api_ref/00overview.html @@ -91,9 +91,9 @@

                                            Quick search

                                            • Release notes
                                                -
                                              • Upcoming (September)
                                              • -
                                              • Latest (04/06/2018)
                                              • +
                                              • Latest (01/10/2018)
                                              • Archived release notes
                                                  +
                                                • sd (04/06/2018)
                                                • sd (04/04/2018)
                                                • sd (27/02/2018)
                                                • sd (12/01/2018)
                                                • diff --git a/docs/API/_build/html/sites/api_ref/Chain.html b/docs/API/_build/html/sites/api_ref/Chain.html index 61348a063..b8893065b 100644 --- a/docs/API/_build/html/sites/api_ref/Chain.html +++ b/docs/API/_build/html/sites/api_ref/Chain.html @@ -92,9 +92,9 @@

                                                  Quick search

                                                  • Release notes
                                                      -
                                                    • Upcoming (September)
                                                    • -
                                                    • Latest (04/06/2018)
                                                    • +
                                                    • Latest (01/10/2018)
                                                    • Archived release notes diff --git a/docs/API/_build/html/sites/api_ref/DataSet.html b/docs/API/_build/html/sites/api_ref/DataSet.html index d1a965a12..8b2405e70 100644 --- a/docs/API/_build/html/sites/api_ref/DataSet.html +++ b/docs/API/_build/html/sites/api_ref/DataSet.html @@ -92,9 +92,9 @@

                                                      Quick search

                                                      • Release notes
                                                          -
                                                        • Upcoming (September)
                                                        • -
                                                        • Latest (04/06/2018)
                                                        • +
                                                        • Latest (01/10/2018)
                                                        • Archived release notes @@ -1343,7 +1344,7 @@

                                                          DataSet Parameters:
                                                            -
                                                          • stack (quantipy.Stack) – The Stack instance to convert.
                                                          • +
                                                          • stack (quantipy.Stack) – The Stack instance to convert.
                                                          • data_key (str) – The reference name where meta and data information are stored.
                                                          • dk_filter (string, default None) – Filter name if the stack contains more than one filters. If None ‘no_filter’ will be used.
                                                          • diff --git a/docs/API/_build/html/sites/api_ref/QuantipyViews.html b/docs/API/_build/html/sites/api_ref/QuantipyViews.html index 2f9e62e21..74a7eb22b 100644 --- a/docs/API/_build/html/sites/api_ref/QuantipyViews.html +++ b/docs/API/_build/html/sites/api_ref/QuantipyViews.html @@ -92,9 +92,9 @@

                                                            Quick search

                                                            • Release notes
                                                                -
                                                              • Upcoming (September)
                                                              • -
                                                              • Latest (04/06/2018)
                                                              • +
                                                              • Latest (01/10/2018)
                                                              • Archived release notes diff --git a/docs/API/_build/html/sites/api_ref/Rim_scheme.html b/docs/API/_build/html/sites/api_ref/Rim_scheme.html index bc3ad02a9..a61efd058 100644 --- a/docs/API/_build/html/sites/api_ref/Rim_scheme.html +++ b/docs/API/_build/html/sites/api_ref/Rim_scheme.html @@ -37,7 +37,7 @@ - + @@ -92,9 +92,9 @@

                                                                Quick search

                                                                • Release notes
                                                                    -
                                                                  • Upcoming (September)
                                                                  • -
                                                                  • Latest (04/06/2018)
                                                                  • +
                                                                  • Latest (01/10/2018)
                                                                  • Archived release notes @@ -465,7 +466,7 @@

                                                                    Rim - Next + Next Previous diff --git a/docs/API/_build/html/sites/api_ref/View.html b/docs/API/_build/html/sites/api_ref/View.html index 660ab38d1..226848b81 100644 --- a/docs/API/_build/html/sites/api_ref/View.html +++ b/docs/API/_build/html/sites/api_ref/View.html @@ -38,7 +38,7 @@ - + @@ -92,9 +92,9 @@

                                                                    Quick search

                                                                    • Release notes
                                                                        -
                                                                      • Upcoming (September)
                                                                      • -
                                                                      • Latest (04/06/2018)
                                                                      • +
                                                                      • Latest (01/10/2018)
                                                                      • Archived release notes @@ -549,7 +550,7 @@

                                                                        ViewNext - Previous + Previous diff --git a/docs/API/_build/html/sites/api_ref/quantify_engine.html b/docs/API/_build/html/sites/api_ref/quantify_engine.html index f8d5d7226..fa88687ea 100644 --- a/docs/API/_build/html/sites/api_ref/quantify_engine.html +++ b/docs/API/_build/html/sites/api_ref/quantify_engine.html @@ -92,9 +92,9 @@

                                                                        Quick search

                                                                        • Release notes
                                                                            -
                                                                          • Upcoming (September)
                                                                          • -
                                                                          • Latest (04/06/2018)
                                                                          • +
                                                                          • Latest (01/10/2018)
                                                                          • Archived release notes diff --git a/docs/API/_build/html/sites/api_ref/stack.html b/docs/API/_build/html/sites/api_ref/stack.html index 75f4b3018..4fc379476 100644 --- a/docs/API/_build/html/sites/api_ref/stack.html +++ b/docs/API/_build/html/sites/api_ref/stack.html @@ -92,9 +92,9 @@

                                                                            Quick search

                                                                            • Release notes
                                                                                -
                                                                              • Upcoming (September)
                                                                              • -
                                                                              • Latest (04/06/2018)
                                                                              • +
                                                                              • Latest (01/10/2018)
                                                                              • Archived release notes
                                                                                  +
                                                                                • sd (04/06/2018)
                                                                                • sd (04/04/2018)
                                                                                • sd (27/02/2018)
                                                                                • sd (12/01/2018)
                                                                                • diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/00_overview.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/00_overview.html index c64c413ea..2f8c59a3f 100644 --- a/docs/API/_build/html/sites/lib_doc/dataprocessing/00_overview.html +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/00_overview.html @@ -91,9 +91,9 @@

                                                                                  Quick search

                                                                                  • Release notes
                                                                                      -
                                                                                    • Upcoming (September)
                                                                                    • -
                                                                                    • Latest (04/06/2018)
                                                                                    • +
                                                                                    • Latest (01/10/2018)
                                                                                    • Archived release notes diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/01_components.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/01_components.html index 0d88da0b6..ee082ed24 100644 --- a/docs/API/_build/html/sites/lib_doc/dataprocessing/01_components.html +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/01_components.html @@ -92,9 +92,9 @@

                                                                                      Quick search

                                                                                      • Release notes
                                                                                          -
                                                                                        • Upcoming (September)
                                                                                        • -
                                                                                        • Latest (04/06/2018)
                                                                                        • +
                                                                                        • Latest (01/10/2018)
                                                                                        • Archived release notes diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html index 2ff1e17ae..e0459efe1 100644 --- a/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/02_io.html @@ -92,9 +92,9 @@

                                                                                          Quick search

                                                                                          • Release notes
                                                                                              -
                                                                                            • Upcoming (September)
                                                                                            • -
                                                                                            • Latest (04/06/2018)
                                                                                            • +
                                                                                            • Latest (01/10/2018)
                                                                                            • Archived release notes diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html index 5d5383bdd..d80c3ae84 100644 --- a/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/02a_management.html @@ -92,9 +92,9 @@

                                                                                              Quick search

                                                                                              • Release notes
                                                                                                  -
                                                                                                • Upcoming (September)
                                                                                                • -
                                                                                                • Latest (04/06/2018)
                                                                                                • +
                                                                                                • Latest (01/10/2018)
                                                                                                • Archived release notes diff --git a/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html b/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html index 38f0bd22d..a8e6ce9f3 100644 --- a/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html +++ b/docs/API/_build/html/sites/lib_doc/dataprocessing/03_inspection.html @@ -92,9 +92,9 @@

                                                                                                  Quick search

                                                                                                  • Release notes
                                                                                                      -
                                                                                                    • Upcoming (September)
                                                                                                    • -
                                                                                                    • Latest (04/06/2018)
                                                                                                    • +
                                                                                                    • Latest (01/10/2018)
                                                                                                    • Archived release notes diff --git a/docs/API/_build/html/sites/release_notes/00_overview.html b/docs/API/_build/html/sites/release_notes/00_overview.html index 696833669..1da0b13ef 100644 --- a/docs/API/_build/html/sites/release_notes/00_overview.html +++ b/docs/API/_build/html/sites/release_notes/00_overview.html @@ -36,7 +36,7 @@ href="../../genindex.html"/> - + @@ -91,9 +91,9 @@

                                                                                                      Quick search

                                                                                                      • Release notes
                                                                                                          -
                                                                                                        • Upcoming (September)
                                                                                                        • -
                                                                                                        • Latest (04/06/2018)
                                                                                                        • +
                                                                                                        • Latest (01/10/2018)
                                                                                                        • Archived release notes
                                                                                                            +
                                                                                                          • sd (04/06/2018)
                                                                                                          • sd (04/04/2018)
                                                                                                          • sd (27/02/2018)
                                                                                                          • sd (12/01/2018)
                                                                                                          • @@ -353,9 +353,9 @@

                                                                                                            Quick search

                                                                                                            Release notes

                                                                                                              -
                                                                                                            • Upcoming (September)
                                                                                                            • -
                                                                                                            • Latest (04/06/2018)
                                                                                                            • +
                                                                                                            • Latest (01/10/2018)
                                                                                                            • Archived release notes
                                                                                                                +
                                                                                                              • sd (04/06/2018)
                                                                                                              • sd (04/04/2018)
                                                                                                              • sd (27/02/2018)
                                                                                                              • sd (12/01/2018)
                                                                                                              • @@ -397,7 +397,7 @@

                                                                                                                Release notes - Next + Next Previous diff --git a/docs/API/_build/html/sites/release_notes/01_latest.html b/docs/API/_build/html/sites/release_notes/01_latest.html index 57f70ecda..7e73e6dd7 100644 --- a/docs/API/_build/html/sites/release_notes/01_latest.html +++ b/docs/API/_build/html/sites/release_notes/01_latest.html @@ -8,7 +8,7 @@ - Upcoming (September) — Quantipy 0.1.3 documentation + Latest (01/10/2018) — Quantipy 0.1.3 documentation @@ -92,9 +92,9 @@

                                                                                                                Quick search

                                                                                                                • Release notes
                                                                                                                    -
                                                                                                                  • Upcoming (September)
                                                                                                                  • -
                                                                                                                  • Latest (04/06/2018)
                                                                                                                  • +
                                                                                                                  • Latest (01/10/2018)
                                                                                                                  • Archived release notes
                                                                                                                      +
                                                                                                                    • sd (04/06/2018)
                                                                                                                    • sd (04/04/2018)
                                                                                                                    • sd (27/02/2018)
                                                                                                                    • sd (12/01/2018)
                                                                                                                    • @@ -338,7 +338,7 @@

                                                                                                                      Quick search

                                                                                                                    • Release notes »
                                                                                                                    • -
                                                                                                                    • Upcoming (September)
                                                                                                                    • +
                                                                                                                    • Latest (01/10/2018)
                                                                                                                    • @@ -354,8 +354,35 @@

                                                                                                                      Quick search

                                                                                                                      -
                                                                                                                      -

                                                                                                                      Upcoming (September)

                                                                                                                      +
                                                                                                                      +

                                                                                                                      Latest (01/10/2018)

                                                                                                                      +

                                                                                                                      New: “rewrite” of Rules module (affecting sorting):

                                                                                                                      +

                                                                                                                      sorting “normal” columns:

                                                                                                                      +
                                                                                                                        +
                                                                                                                      • sort_on always ‘@’
                                                                                                                      • +
                                                                                                                      • fix any categories
                                                                                                                      • +
                                                                                                                      • sort_by_weight default is unweighted (None), but each weight (included
                                                                                                                      • +
                                                                                                                      +

                                                                                                                      in data) can be used

                                                                                                                      +

                                                                                                                      If sort_by_weight and the view-weight differ, a warning is shown.

                                                                                                                      +

                                                                                                                      sorting “expanded net” columns:

                                                                                                                      +
                                                                                                                        +
                                                                                                                      • sort_on always ‘@’
                                                                                                                      • +
                                                                                                                      • fix any categories
                                                                                                                      • +
                                                                                                                      • sorting within or between net groups is available
                                                                                                                      • +
                                                                                                                      • sort_by_weight: as default the weight of the first found
                                                                                                                      • +
                                                                                                                      +

                                                                                                                      expanded-net-view is taken. Only weights of aggregated net-views are possible

                                                                                                                      +

                                                                                                                      sorting “array summaries”:

                                                                                                                      +
                                                                                                                        +
                                                                                                                      • sort_on can be any desc (‘median’, ‘stddev’, ‘sem’, ‘max’, ‘min’,
                                                                                                                      • +
                                                                                                                      +

                                                                                                                      ‘mean’, ‘upper_q’, ‘lower_q’) or nets (‘net_1’, ‘net_2’, …. enumerated +by the net_def) +* sort_by_weight: as default the weight of the first found desc/net-view +is taken. Only weights of aggregated desc/net-views are possible +* sort_on can also be any category, here each weight can be used to sort_on

                                                                                                                      +

                                                                                                                      New: DataSet.min_value_count()

                                                                                                                      A new wrapper for DataSet.hiding() is included. All values are hidden, that have less counts than the included number min. @@ -420,66 +447,7 @@

                                                                                                                      Upcoming (September)Batch.extend_x()

                                                                                                                      The method automatically checks if the included variables are arrays and adds them to Batch.summaries if they are included yet.

                                                                                                                      -


                                                                                                                      -
                                                                                                                      -

                                                                                                                      Latest (04/06/2018)

                                                                                                                      -

                                                                                                                      New: Additional variable (names) “getter”-like and resolver methods

                                                                                                                      -
                                                                                                                        -
                                                                                                                      • DataSet.created()
                                                                                                                      • -
                                                                                                                      • DataSet.find(str_tags=None, suffixed=False)
                                                                                                                      • -
                                                                                                                      • DataSet.names()
                                                                                                                      • -
                                                                                                                      • DataSet.resolve_name()
                                                                                                                      • -
                                                                                                                      -

                                                                                                                      A bunch of new methods enhancing the options of finding and testing for variable -names have been added. created() will list all variables that have been added -to a dataset using core functions, i.e. add_meta() and derive(), resp. -all helper methods that use them internally (as band() or categorize() do -for instance).

                                                                                                                      -

                                                                                                                      The find() method is returning all variable names that contain any of the -provided substrings in str_tags. To only consider names that end with these -strings, set suffixed=True. If no str_tags are passed, the method will -use a default list of tags including ['_rc', '_net', ' (categories', ' (NET', '_rec'].

                                                                                                                      -

                                                                                                                      Sometimes a dataset might contain “semi-duplicated” names, variables that differ -in respect to case sensitivity but have otherwise identical names. Calling -names() will report such cases in a pd.DataFrame that lists all name -variants under the respective str.lower() version. If no semi-duplicates -are found, names() will simply return DataSet.variables().

                                                                                                                      -

                                                                                                                      Lastly, resolve_name() can be used to return the “proper”, existing representation(s) of a given variable name’s spelling.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      New: Batch.remove()

                                                                                                                      -

                                                                                                                      Not needed batches can be removed from meta, so they are not aggregated -anymore.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      New: Batch.rename(new_name)

                                                                                                                      -

                                                                                                                      Sometimes standard batches have long/ complex names. They can now be changed -into a custom name. Please take into account, that for most hubs the name of -omnibus batches should look like ‘client ~ topic’.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      Update: Handling verbatims in qp.Batch

                                                                                                                      -

                                                                                                                      Instead of holding the well prepared open-end dataframe in batch.verbatims, -the attribute is now filled by batch.add_open_ends() with instructions to -create the open-end dataframe. It is easier to to modify/ overwrite existing -verbatims. Therefore also a new parameter is included overwrite=True.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      Update: Batch.copy(..., b_filter=None, as_addition=False)

                                                                                                                      -

                                                                                                                      It is now possible to define an additional filter for a copied batch and also -to set it as addition to the master batch.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      Update: Regrouping the variable list using DataSet.order(..., regroup=True)

                                                                                                                      -

                                                                                                                      A new parameter called regroup will instruct reordering all newly created -variables into their logical position of the dataset’s main variable order, i.e. -attempting to place derived variables after the originating ones.

                                                                                                                      -
                                                                                                                      -

                                                                                                                      Bugfix: add_meta() and duplicated categorical values codes

                                                                                                                      -

                                                                                                                      Providing duplicated numerical codes while attempting to create new metadata -using add_meta() will now correctly raise a ValueError to prevent -corrupting the DataSet.

                                                                                                                      -
                                                                                                                      >>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')]
                                                                                                                      ->>> dataset.add_meta('test_var', 'single', 'test label', cats)
                                                                                                                      -ValueError: Cannot resolve category definition due to code duplicates: [2, 3]
                                                                                                                      -
                                                                                                                      -
                                                                                                                      diff --git a/docs/API/_build/html/sites/release_notes/02_archive.html b/docs/API/_build/html/sites/release_notes/02_archive.html index 653dfb55f..e0c791e03 100644 --- a/docs/API/_build/html/sites/release_notes/02_archive.html +++ b/docs/API/_build/html/sites/release_notes/02_archive.html @@ -38,7 +38,7 @@ - + @@ -92,9 +92,9 @@

                                                                                                                      Quick search

                                                                                                                      Archived release notes

                                                                                                                      +
                                                                                                                      +

                                                                                                                      sd (04/06/2018)

                                                                                                                      +

                                                                                                                      New: Additional variable (names) “getter”-like and resolver methods

                                                                                                                      +
                                                                                                                        +
                                                                                                                      • DataSet.created()
                                                                                                                      • +
                                                                                                                      • DataSet.find(str_tags=None, suffixed=False)
                                                                                                                      • +
                                                                                                                      • DataSet.names()
                                                                                                                      • +
                                                                                                                      • DataSet.resolve_name()
                                                                                                                      • +
                                                                                                                      +

                                                                                                                      A bunch of new methods enhancing the options of finding and testing for variable +names have been added. created() will list all variables that have been added +to a dataset using core functions, i.e. add_meta() and derive(), resp. +all helper methods that use them internally (as band() or categorize() do +for instance).

                                                                                                                      +

                                                                                                                      The find() method is returning all variable names that contain any of the +provided substrings in str_tags. To only consider names that end with these +strings, set suffixed=True. If no str_tags are passed, the method will +use a default list of tags including ['_rc', '_net', ' (categories', ' (NET', '_rec'].

                                                                                                                      +

                                                                                                                      Sometimes a dataset might contain “semi-duplicated” names, variables that differ +in respect to case sensitivity but have otherwise identical names. Calling +names() will report such cases in a pd.DataFrame that lists all name +variants under the respective str.lower() version. If no semi-duplicates +are found, names() will simply return DataSet.variables().

                                                                                                                      +

                                                                                                                      Lastly, resolve_name() can be used to return the “proper”, existing representation(s) of a given variable name’s spelling.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      New: Batch.remove()

                                                                                                                      +

                                                                                                                      Not needed batches can be removed from meta, so they are not aggregated +anymore.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      New: Batch.rename(new_name)

                                                                                                                      +

                                                                                                                      Sometimes standard batches have long/ complex names. They can now be changed +into a custom name. Please take into account, that for most hubs the name of +omnibus batches should look like ‘client ~ topic’.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      Update: Handling verbatims in qp.Batch

                                                                                                                      +

                                                                                                                      Instead of holding the well prepared open-end dataframe in batch.verbatims, +the attribute is now filled by batch.add_open_ends() with instructions to +create the open-end dataframe. It is easier to to modify/ overwrite existing +verbatims. Therefore also a new parameter is included overwrite=True.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      Update: Batch.copy(..., b_filter=None, as_addition=False)

                                                                                                                      +

                                                                                                                      It is now possible to define an additional filter for a copied batch and also +to set it as addition to the master batch.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      Update: Regrouping the variable list using DataSet.order(..., regroup=True)

                                                                                                                      +

                                                                                                                      A new parameter called regroup will instruct reordering all newly created +variables into their logical position of the dataset’s main variable order, i.e. +attempting to place derived variables after the originating ones.

                                                                                                                      +
                                                                                                                      +

                                                                                                                      Bugfix: add_meta() and duplicated categorical values codes

                                                                                                                      +

                                                                                                                      Providing duplicated numerical codes while attempting to create new metadata +using add_meta() will now correctly raise a ValueError to prevent +corrupting the DataSet.

                                                                                                                      +
                                                                                                                      >>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')]
                                                                                                                      +>>> dataset.add_meta('test_var', 'single', 'test label', cats)
                                                                                                                      +ValueError: Cannot resolve category definition due to code duplicates: [2, 3]
                                                                                                                      +
                                                                                                                      +
                                                                                                                      +

                                                                                                                      sd (04/04/2018)

                                                                                                                      New: Emptiness handlers in DataSet and Batch classes

                                                                                                                      @@ -1450,7 +1510,7 @@

                                                                                                                      sd (09/11/2016)Next - Previous + Previous

                                                                                                                      diff --git a/docs/API/sites/release_notes/01_latest.rst b/docs/API/sites/release_notes/01_latest.rst index c614f5647..4eb81c383 100644 --- a/docs/API/sites/release_notes/01_latest.rst +++ b/docs/API/sites/release_notes/01_latest.rst @@ -2,9 +2,39 @@ :maxdepth: 5 :includehidden: -==================== -Upcoming (September) -==================== +=================== +Latest (01/10/2018) +=================== + +**New**: "rewrite" of Rules module (affecting sorting): + +**sorting "normal" columns**: + +* ``sort_on`` always '@' +* ``fix`` any categories +* ``sort_by_weight`` default is unweighted (None), but each weight (included +in data) can be used + +If sort_by_weight and the view-weight differ, a warning is shown. + +**sorting "expanded net" columns**: + +* ``sort_on`` always '@' +* ``fix`` any categories +* sorting ``within`` or ``between`` net groups is available +* ``sort_by_weight``: as default the weight of the first found +expanded-net-view is taken. Only weights of aggregated net-views are possible + +**sorting "array summaries"**: + +* ``sort_on`` can be any desc ('median', 'stddev', 'sem', 'max', 'min', +'mean', 'upper_q', 'lower_q') or nets ('net_1', 'net_2', .... enumerated +by the net_def) +* ``sort_by_weight``: as default the weight of the first found desc/net-view +is taken. Only weights of aggregated desc/net-views are possible +* ``sort_on`` can also be any category, here each weight can be used to sort_on + +"""" **New**: ``DataSet.min_value_count()`` @@ -102,85 +132,3 @@ The method automatically checks if the included variables are arrays and adds them to ``Batch.summaries`` if they are included yet. """" - - -=================== -Latest (04/06/2018) -=================== - -**New**: Additional variable (names) "getter"-like and resolver methods - -* ``DataSet.created()`` -* ``DataSet.find(str_tags=None, suffixed=False)`` -* ``DataSet.names()`` -* ``DataSet.resolve_name()`` - -A bunch of new methods enhancing the options of finding and testing for variable -names have been added. ``created()`` will list all variables that have been added -to a dataset using core functions, i.e. ``add_meta()`` and ``derive()``, resp. -all helper methods that use them internally (as ``band()`` or ``categorize()`` do -for instance). - -The ``find()`` method is returning all variable names that contain any of the -provided substrings in ``str_tags``. To only consider names that end with these -strings, set ``suffixed=True``. If no ``str_tags`` are passed, the method will -use a default list of tags including ``['_rc', '_net', ' (categories', ' (NET', '_rec']``. - -Sometimes a dataset might contain "semi-duplicated" names, variables that differ -in respect to case sensitivity but have otherwise identical names. Calling -``names()`` will report such cases in a ``pd.DataFrame`` that lists all name -variants under the respective ``str.lower()`` version. If no semi-duplicates -are found, ``names()`` will simply return ``DataSet.variables()``. - -Lastly, ``resolve_name()`` can be used to return the "proper", existing representation(s) of a given variable name's spelling. - -"""" - -**New**: ``Batch.remove()`` - -Not needed batches can be removed from ``meta``, so they are not aggregated -anymore. - -"""" - -**New**: ``Batch.rename(new_name)`` - -Sometimes standard batches have long/ complex names. They can now be changed -into a custom name. Please take into account, that for most hubs the name of -omnibus batches should look like 'client ~ topic'. - -"""" - -**Update**: Handling verbatims in ``qp.Batch`` - -Instead of holding the well prepared open-end dataframe in ``batch.verbatims``, -the attribute is now filled by ``batch.add_open_ends()`` with instructions to -create the open-end dataframe. It is easier to to modify/ overwrite existing -verbatims. Therefore also a new parameter is included ``overwrite=True``. - -"""" - -**Update**: ``Batch.copy(..., b_filter=None, as_addition=False)`` - -It is now possible to define an additional filter for a copied batch and also -to set it as addition to the master batch. - -"""" - -**Update**: Regrouping the variable list using ``DataSet.order(..., regroup=True)`` - -A new parameter called ``regroup`` will instruct reordering all newly created -variables into their logical position of the dataset's main variable order, i.e. -attempting to place *derived* variables after the *originating* ones. - -"""" - -**Bugfix**: ``add_meta()`` and duplicated categorical ``values`` codes - -Providing duplicated numerical codes while attempting to create new metadata -using ``add_meta()`` will now correctly raise a ``ValueError`` to prevent -corrupting the ``DataSet``. - ->>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')] ->>> dataset.add_meta('test_var', 'single', 'test label', cats) -ValueError: Cannot resolve category definition due to code duplicates: [2, 3] diff --git a/docs/API/sites/release_notes/02_archive.rst b/docs/API/sites/release_notes/02_archive.rst index 179034afd..d2099d199 100644 --- a/docs/API/sites/release_notes/02_archive.rst +++ b/docs/API/sites/release_notes/02_archive.rst @@ -8,6 +8,91 @@ Archived release notes ====================== +--------------- +sd (04/06/2018) +--------------- + + + +**New**: Additional variable (names) "getter"-like and resolver methods + +* ``DataSet.created()`` +* ``DataSet.find(str_tags=None, suffixed=False)`` +* ``DataSet.names()`` +* ``DataSet.resolve_name()`` + +A bunch of new methods enhancing the options of finding and testing for variable +names have been added. ``created()`` will list all variables that have been added +to a dataset using core functions, i.e. ``add_meta()`` and ``derive()``, resp. +all helper methods that use them internally (as ``band()`` or ``categorize()`` do +for instance). + +The ``find()`` method is returning all variable names that contain any of the +provided substrings in ``str_tags``. To only consider names that end with these +strings, set ``suffixed=True``. If no ``str_tags`` are passed, the method will +use a default list of tags including ``['_rc', '_net', ' (categories', ' (NET', '_rec']``. + +Sometimes a dataset might contain "semi-duplicated" names, variables that differ +in respect to case sensitivity but have otherwise identical names. Calling +``names()`` will report such cases in a ``pd.DataFrame`` that lists all name +variants under the respective ``str.lower()`` version. If no semi-duplicates +are found, ``names()`` will simply return ``DataSet.variables()``. + +Lastly, ``resolve_name()`` can be used to return the "proper", existing representation(s) of a given variable name's spelling. + +"""" + +**New**: ``Batch.remove()`` + +Not needed batches can be removed from ``meta``, so they are not aggregated +anymore. + +"""" + +**New**: ``Batch.rename(new_name)`` + +Sometimes standard batches have long/ complex names. They can now be changed +into a custom name. Please take into account, that for most hubs the name of +omnibus batches should look like 'client ~ topic'. + +"""" + +**Update**: Handling verbatims in ``qp.Batch`` + +Instead of holding the well prepared open-end dataframe in ``batch.verbatims``, +the attribute is now filled by ``batch.add_open_ends()`` with instructions to +create the open-end dataframe. It is easier to to modify/ overwrite existing +verbatims. Therefore also a new parameter is included ``overwrite=True``. + +"""" + +**Update**: ``Batch.copy(..., b_filter=None, as_addition=False)`` + +It is now possible to define an additional filter for a copied batch and also +to set it as addition to the master batch. + +"""" + +**Update**: Regrouping the variable list using ``DataSet.order(..., regroup=True)`` + +A new parameter called ``regroup`` will instruct reordering all newly created +variables into their logical position of the dataset's main variable order, i.e. +attempting to place *derived* variables after the *originating* ones. + +"""" + +**Bugfix**: ``add_meta()`` and duplicated categorical ``values`` codes + +Providing duplicated numerical codes while attempting to create new metadata +using ``add_meta()`` will now correctly raise a ``ValueError`` to prevent +corrupting the ``DataSet``. + +>>> cats = [(1, 'A'), (2, 'B'), (3, 'C'), (3, 'D'), (2, 'AA')] +>>> dataset.add_meta('test_var', 'single', 'test label', cats) +ValueError: Cannot resolve category definition due to code duplicates: [2, 3] + + + --------------- sd (04/04/2018) --------------- From f3d996eef75810bb5be5e2fe80551fb0d052b35d Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Mon, 1 Oct 2018 10:44:04 +0200 Subject: [PATCH 498/733] doc strings --- quantipy/sandbox/pptx/PptxChainClass.py | 2 +- quantipy/sandbox/pptx/PptxPainterClass.py | 155 ++++++++++++++++------ 2 files changed, 114 insertions(+), 43 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 8d4e03829..4308ab18d 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -338,7 +338,7 @@ def to_table(self, decimals=2, pct_decimals=2): return self - def _select_categories(self,categories): + def _select_categories(self, categories): """ Returns a copy of self.df having only the categories requested diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index b883dea04..6ffea72c3 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -44,10 +44,10 @@ def chartdata_from_dataframe(df, number_format="0%", xl_number_format='0.00%'): df : pandas.DataFrame The dataframe instance from which ChartData will be created. number_format : str, default="0%" - The pptx number format for the intended ChartData. See: - http://python-pptx.readthedocs.io/en/latest/api/chart-data.html?highlight=number_format#pptx.chart.data.CategoryChartData.number_format + The pptx number format for the intended ChartData. See: + http://python-pptx.readthedocs.io/en/latest/api/chart-data.html?highlight=number_format#pptx.chart.data.CategoryChartData.number_format xl_number_format : str, default="0.00%" - The xlsx number format for the Excel sheet behind the intended Chart. See: + The xlsx number format for the Excel sheet behind the intended Chart. See: Returns ------- @@ -119,22 +119,22 @@ def return_slide_layout_by_name(pptx, slide_layout_name): class PptxPainter(object): """ A convenience wrapper around the python-pptx library + + Makes a Presentation instance and also defines a default slide layout if specified. + + Parameters + ---------- + path_to_presentation: str + Full path to PowerPoint template + slide_layout: int + A PowerPoint slide layout. + To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. + shape_properties: quantipy.sandbox.pptx.PptxDefaultsClass.PptxDefaults + An instance of PptxDefaults + """ def __init__(self, path_to_presentation, slide_layout=None, shape_properties=None): - """ - Makes a Presentation instance and also defines a default slide layout if specified. - - Parameters - ---------- - path_to_presentation: str - Full path to PowerPoint book - slide_layout: int - A PowerPoint slide layout. - To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. - shape_properties: quantipy.sandbox.pptx.PptxDefaultsClass.PptxDefaults - An instance of PptxDefaults - """ self.presentation = Presentation(path_to_presentation) # TODO PptxPainter - Path checking # type: Presentation if slide_layout is None: @@ -172,19 +172,41 @@ def __init__(self, path_to_presentation, slide_layout=None, shape_properties=Non def queue_slide_items(self, pptx_chain, slide_items): """ Helper function to queue a full automated slide. - Includes queueing of header with question text, chart and footer with base description + Includes queueing of header with question text, a table or chart with optional side table, + and footer with base description. Parameters ---------- pptx_chain: quantipy.sandbox.pptx.PptxChainClass.PptxChain An instance of a PptxChain slide_items: basestring - A string of slide items, separated with +, eg. 'chart+table' - Supported items are 'chart' and 'table' - An item cannot appear more than once + A string of slide items with cell types in the form 'slide_item:cell_types'. + Available slide items are: + 'chart' + 'sidetable' + 'table' + Every slide item needs a comma separated list of cell types to include in the chart or table. + Available cell types are: 'pct, net, mean, stats, tests' + + A slide_items_string could look like: 'chart:pct,net' + Separate multiple slide items with '+' eg. : 'chart:pct+side_table:net' + One slide item type can only appear once in a slide_items_string. + Returns ------- None + calls: + self.draft_textbox_header() + self.draft_textbox_footer() + self.queue_textbox() + self.draft_table() + self.queue_table() + self.draft_side_table() + self.queue_side_table() + self.draft_autochart() + self.queue_chart() + self._check_shapes() + """ valid_slide_items = ['chart','table','side_table'] @@ -229,7 +251,22 @@ def queue_slide_items(self, pptx_chain, slide_items): return None - def _check_shapes(self,adjust='chart'): + def _check_shapes(self, adjust='chart'): + """ + Purpose is to check and adjust all queued items for any collisions: + Currently checks if charts and side_tables colide and then adjust the chart. + + Parameters + ---------- + adjust: str + A not implemented future option to select what to adjust. + + Returns + ------- + None + edits self.slide_kwargs + + """ # Find the side_table with the lowest 'left' number table_max_left=12240000 @@ -247,36 +284,61 @@ def _check_shapes(self,adjust='chart'): def clear_tables(self): """ Initilalize the slide_kwargs "tables" dict - :return: None, removes all keys from self.slide_kwargs['tables'] + + Returns + ------- + None + Removes all keys from self.slide_kwargs['tables'] """ self.clear_queue('tables') def clear_side_tables(self): """ - Initilalize the slide_kwargs "tables" dict - :return: None, removes all keys from self.slide_kwargs['tables'] + Initilalize the slide_kwargs "side_tables" dict + + Returns + ------- + None + Removes all keys from self.slide_kwargs['side_tables'] """ self.clear_queue('side_tables') def clear_charts(self): """ Initilalize the slide_kwargs "charts" dict - :return: None, removes all keys from self.slide_kwargs['charts'] + + Returns + ------- + None + Removes all keys from self.slide_kwargs['charts'] """ self.clear_queue('charts') def clear_textboxes(self): """ - Initilalize the slide_kwargs "txtboxes" dict - :return: None, removes all keys from self.slide_kwargs['txtboxes'] + Initilalize the slide_kwargs "text_boxes" dict + + Returns + ------- + None + Removes all keys from self.slide_kwargs['text_boxes'] """ self.clear_queue('textboxs') def clear_queue(self, key): """ - Initialize the shape dicts in slide_kwargs - :param key: String ('all', 'charts', textboxes','tables') - :return: None, removes all keys from requested dict in self.slide_kwargs + Initialize the requested shape dict in slide_kwargs + + Parameters + ---------- + key: str + 'all', 'charts', textboxes','tables', 'side_tables' + + Returns + ------- + None + Removes requested keys from self.slide_kwargs + """ if key=='all': for item in self.slide_kwargs.keys(): @@ -292,11 +354,18 @@ def clear_queue(self, key): def set_slide_layout(self, slide_layout): """ - Method to set a Slide Layout - :param - slide_layout: Int - To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. - :return: Instance of SlideLayout set to the specified slide layout + Method to set a Slide Layout. + + Parameters + ---------- + slide_layout: int or str + To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. + + Returns + ------- + pptx.slide.SlideLayout + Instance of SlideLayout set to the specified slide layout + """ if isinstance(slide_layout, int): return self.presentation.slide_layouts[slide_layout] @@ -305,14 +374,16 @@ def set_slide_layout(self, slide_layout): def add_slide(self, slide_layout=None): """ - Method that creates a Slide instance - :param - slide_layout: Int - The Slide Layout to use. - To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. - If no Slide layout is specified then self.default_slide_layout will be used + Method that creates a Slide instance. + + Parameters + ---------- + slide_layout: int or str + To see available Slide Layouts in a PPTX, select the Viev menu and click Slide Master. + + Returns + ------- - :return: None - sets the self.slide property """ if slide_layout is None: if self.default_slide_layout is None: From 8d25c7bcbd859b3d7e6fbe874ff6f616ddd7fc0f Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Mon, 1 Oct 2018 11:55:03 +0200 Subject: [PATCH 499/733] adjust warning condition in rules sorting --- quantipy/core/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 8ab4e54f7..cd74b900c 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -223,7 +223,7 @@ def _get_frequency_via_stack(self, col, axis, weight): view_weight = self.view_name.split('|')[-2] link_weights = [k.split('|')[-2] for k in self.link.keys() if not 'base' in k.split('|')[-1]] - if not (weight == view_weight or weight not in link_weights): + if not (weight == view_weight or weight in link_weights): msg = "\n{}: view-weight and weight to sort on differ ('{}' vs '{}')\n" warnings.warn(msg.format(col, view_weight, weight or None)) try: From a4bdd02a7d6f1081a52ea649051c8e62554fd1c0 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Mon, 1 Oct 2018 14:31:19 +0200 Subject: [PATCH 500/733] remove unused modules --- quantipy/sandbox/pptx/PptxChainClass.py | 1 - quantipy/sandbox/pptx/pptx_defaults.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 4308ab18d..33d0317d3 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -3,7 +3,6 @@ import warnings import numpy as np import pandas as pd -from quantipy.sandbox.sandbox import Chain import topy.core.ygpy.tools as t t.use_encoding('utf-8') diff --git a/quantipy/sandbox/pptx/pptx_defaults.py b/quantipy/sandbox/pptx/pptx_defaults.py index 9540e0a62..9b3a00264 100644 --- a/quantipy/sandbox/pptx/pptx_defaults.py +++ b/quantipy/sandbox/pptx/pptx_defaults.py @@ -1,11 +1,5 @@ # encoding: utf-8 -# from pptx.util import ( -# Emu, -# Pt, -# Cm, -# Inches) - from collections import OrderedDict import pandas as pd From 699df5f66bd7336f4d6679d23d34c29abe436559 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Mon, 1 Oct 2018 14:42:39 +0200 Subject: [PATCH 501/733] bug in import --- quantipy/sandbox/pptx/PptxChainClass.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 33d0317d3..c610badc2 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -3,8 +3,14 @@ import warnings import numpy as np import pandas as pd -import topy.core.ygpy.tools as t -t.use_encoding('utf-8') +import sys + +default_stdout = sys.stdout +default_stderr = sys.stderr +reload(sys) +sys.setdefaultencoding('utf-8') +sys.stdout = default_stdout +sys.stderr = default_stderr BASE_COL = '@' BASE_ROW = ['is_counts', 'is_c_base'] From 91817311a23b5092dab152e8c668c0278632687e Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Mon, 1 Oct 2018 15:16:43 +0200 Subject: [PATCH 502/733] fix conflict with branch develop --- quantipy/core/rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 33c4827ad..cd74b900c 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -621,7 +621,8 @@ def verify_test_value(value): if len(value)==1: value = set(value) else: - value = set([int(i) for i in list(value[1:-1].split(','))]) + value = set([int(i) if i.isdigit() else i + for i in list(value[1:-1].split(','))]) value = cols.intersection(value) if not value: value = '' From 3f5607bc21403a785fe54a54a1b7dcede4a0a04e Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 2 Oct 2018 08:45:51 +0200 Subject: [PATCH 503/733] new code to detect numeric arrays --- quantipy/core/stack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 394ef0e30..49bde9829 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -1973,6 +1973,9 @@ def aggregate(self, views, unweighted_base=True, categorize=[], xs = [x for x in xs if x in x_in_stack or isinstance(x, tuple)] v_typ = self.variable_types(dk, verbose=False) numerics = v_typ['int'] + v_typ['float'] + masks = self[dk].meta['masks'] + num_arrays = [m for m in masks if masks[m]['subtype'] in ['int', 'float']] + if num_arrays: numerics = numerics + num_arrays skipped = [x for x in xs if (x in numerics and not x in categorize) and not isinstance(x, tuple)] total_len = len(xs) From 56359c6dea0e26ca4bbd5ebe1079ada7c61f3aa6 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 2 Oct 2018 11:51:25 +0200 Subject: [PATCH 504/733] new sort_weight utility --- quantipy/core/dataset.py | 2 +- quantipy/core/rules.py | 45 +++++++++++++++++++++++-------------- quantipy/sandbox/sandbox.py | 14 +++++++----- tests/test_dataset.py | 2 +- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index afc2e1939..533abea4d 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6311,7 +6311,7 @@ def hiding(self, name, hide, axis='y', hide_values=True): @modify(to_list=['name', 'fix']) @verify(variables={'name': 'both'}) def sorting(self, name, on='@', within=False, between=False, fix=None, - ascending=False, sort_by_weight=None): + ascending=False, sort_by_weight='auto'): """ Set or update ``rules['x']['sortx']`` meta for the named column. diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index cd74b900c..770d7798f 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -11,7 +11,7 @@ class Rules(object): # ------------------------------------------------------------------------ # init # ------------------------------------------------------------------------ - def __init__(self, link, view_name, axes=['x', 'y']): + def __init__(self, link, view_name, axes=['x', 'y'], rweight=None): self.link = link self.view_name = view_name self.stack_base = link.stack[link.data_key] @@ -26,7 +26,7 @@ def __init__(self, link, view_name, axes=['x', 'y']): self.view_df = link[view_name].dataframe self._xrule_col = None self._yrule_col = None - self._sort_weight = self._get_sort_weight() + self._sort_weight = self._get_sort_weight(rweight) self.x_rules = self._set_rules_params(axes, 'x') self.y_rules = self._set_rules_params(axes, 'y') self.x_slicer = None @@ -76,7 +76,7 @@ def _set_rules_params(self, all_rules_axes, rules_axis): pass return rules - def _get_sort_weight(self): + def _get_sort_weight(self, use_weight): var = self.link.y if self.link.x=='@' else self.link.x try: collection = self.meta['columns'][var] @@ -86,21 +86,32 @@ def _get_sort_weight(self): if 'sortx' in rules: sort_on = rules['sortx'].get('sort_on', '@') sort_weight = rules['sortx']['with_weight'] or '' - if sort_on in ['median', 'stddev', 'sem', 'max', 'min', 'mean', - 'upper_q', 'lower_q']: - desc_weights = [k.split('|')[-2] for k in self.link.keys() - if 'd.{}'.format(sort_on) in k.split('|')[1]] - if not sort_weight in desc_weights: sort_weight = desc_weights[0] - elif 'net' in sort_on: - net_weights = [k.split('|')[-2] for k in self.link.keys() - if k.split('|')[-1] == 'net'] - if not sort_weight in net_weights: sort_weight = net_weights[0] - elif sort_on == '@': - expanded_nets_w = [k.split('|')[-2] for k in self.link.keys() - if '}+]' in k.split('|')[2]] - if expanded_nets_w and not sort_weight in expanded_nets_w: - sort_weight = expanded_nets_w[0] + if sort_weight == 'auto': + if use_weight is None: + sort_weight = self.view_name.split('|')[-2] + else: + sort_weight = use_weight + + # DELETE + # ---------------------------------------------------------------- + # if sort_on in ['median', 'stddev', 'sem', 'max', 'min', 'mean', + # 'upper_q', 'lower_q']: + # desc_weights = [k.split('|')[-2] for k in self.link.keys() + # if 'd.{}'.format(sort_on) in k.split('|')[1]] + # if not sort_weight in desc_weights: sort_weight = desc_weights[0] + # elif 'net' in sort_on: + # net_weights = [k.split('|')[-2] for k in self.link.keys() + # if k.split('|')[-1] == 'net'] + # if not sort_weight in net_weights: sort_weight = net_weights[0] + # elif sort_on == '@': + # expanded_nets_w = [k.split('|')[-2] for k in self.link.keys() + # if '}+]' in k.split('|')[2]] + # if expanded_nets_w and not sort_weight in expanded_nets_w: + # sort_weight = expanded_nets_w[0] + # ---------------------------------------------------------------- + return sort_weight + else: None diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index ad8f9e415..afd4de181 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1363,7 +1363,8 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, orient='x', chain = chain.get(data_key, filter_key, self._force_list(x_key), self._force_list(y_key), views, rules=rules, - prioritize=prioritize, orient=orient) + rules_weight=rules_weight, prioritize=prioritize, + orient=orient) folders = self.folder_names if folder in folders: @@ -2290,7 +2291,7 @@ def _describe_block(self, description, row_id): return block_net_def def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, - orient='x', prioritize=True): + rules_weight=None, orient='x', prioritize=True): """ Get the concatenated Chain.DataFrame """ self._meta = self.stack[data_key].meta @@ -2320,7 +2321,8 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, continue if prioritize: link = self._drop_substituted_views(link) - found_views, y_frames = self._concat_views(link, views) + found_views, y_frames = self._concat_views( + link, views, rules_weight) found.append(found_views) try: @@ -2530,7 +2532,7 @@ def _reindx_source(df, varname, total): df.columns = df.columns.set_levels([varname], level=0, inplace=False) return df - def _concat_views(self, link, views, found=None): + def _concat_views(self, link, views, rules_weight, found=None): """ Concatenates the Views of a Chain. """ frames = [] @@ -2561,7 +2563,7 @@ def _concat_views(self, link, views, found=None): else: use_grp_type = self._group_style - found, grouped = self._concat_views(link, view, found=found) + found, grouped = self._concat_views(link, view, rules_weight, found=found) if grouped: frames.append(self._group_views(grouped, use_grp_type)) else: @@ -2611,7 +2613,7 @@ def _concat_views(self, link, views, found=None): rules_weight = None if self._has_rules: - rules = Rules(link, view, axes=self._has_rules) + rules = Rules(link, view, self._has_rules, rules_weight) # print rules.show_rules() # rules.get_slicer() # print rules.show_slicers() diff --git a/tests/test_dataset.py b/tests/test_dataset.py index be2019f2d..28bb0e7f8 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -501,7 +501,7 @@ def test_sorting_rules_meta(self): 'between': False, 'ascending': False, 'sort_on': '@', - 'with_weight': None}}, + 'with_weight': 'auto'}}, 'y': {}} # rule correctly set?: i.e. code 100 removed from fix list since it # does not appear in the values meta? From f8ef577a5d8a170ebf2f9e6d8ca8be03ba635f94 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Tue, 2 Oct 2018 13:52:20 +0200 Subject: [PATCH 505/733] remove pillow from setup.py lib requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index df3d3fd41..438091985 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ 'xmltodict', 'lxml', 'xlsxwriter', - 'pillow', + # 'pillow', 'prettytable', 'decorator', 'watchdog', From c42db645398dba02bfd19fe3960d5194ebc294ba Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 2 Oct 2018 14:40:05 +0200 Subject: [PATCH 506/733] removing commented code --- quantipy/core/rules.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 770d7798f..f96b0fc42 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -91,29 +91,9 @@ def _get_sort_weight(self, use_weight): sort_weight = self.view_name.split('|')[-2] else: sort_weight = use_weight - - # DELETE - # ---------------------------------------------------------------- - # if sort_on in ['median', 'stddev', 'sem', 'max', 'min', 'mean', - # 'upper_q', 'lower_q']: - # desc_weights = [k.split('|')[-2] for k in self.link.keys() - # if 'd.{}'.format(sort_on) in k.split('|')[1]] - # if not sort_weight in desc_weights: sort_weight = desc_weights[0] - # elif 'net' in sort_on: - # net_weights = [k.split('|')[-2] for k in self.link.keys() - # if k.split('|')[-1] == 'net'] - # if not sort_weight in net_weights: sort_weight = net_weights[0] - # elif sort_on == '@': - # expanded_nets_w = [k.split('|')[-2] for k in self.link.keys() - # if '}+]' in k.split('|')[2]] - # if expanded_nets_w and not sort_weight in expanded_nets_w: - # sort_weight = expanded_nets_w[0] - # ---------------------------------------------------------------- - return sort_weight - else: - None + return None # ------------------------------------------------------------------------ # display From e07115d0b60149be31e892f7dab40b27775ae344 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 5 Oct 2018 11:15:03 +0200 Subject: [PATCH 507/733] Allowing int-based value sorting --- quantipy/core/rules.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index f96b0fc42..cc9d44faa 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -168,7 +168,18 @@ def get_slicer(self): # get df (-slice) to apply rule on if 'sortx' in rule_axis: sort_on = rule_axis['sortx'].get('sort_on', '@') + + sort_on_stat = False + sort_on_net = False + + if isinstance(sort_on, (str, unicode)): + sort_on_mean = sort_on in [ + 'median', 'stddev', 'sem', 'max', 'min', 'mean', + 'upper_q', 'lower_q'] + sort_on_net = sort_on.startswith('net') + expanded_net = self._find_expanded_nets(views, rule_axis) + # sort expanded nets if expanded_net and not self.array_summary: if not sort_on == '@': @@ -184,11 +195,10 @@ def get_slicer(self): self.y_slicer = r_slicer return None # get df-desc-slice to sort on - elif sort_on in ['median', 'stddev', 'sem', 'max', 'min', - 'mean', 'upper_q', 'lower_q']: + elif sort_on_stat: f = self._get_descriptive_via_stack(col_key, sort_on) # get df-net-slice to sort on - elif 'net' in sort_on: + elif sort_on_net: f = self._get_net_via_stack(col_key, sort_on) # get df-freq-slice to sort on else: From 6d29f8a7404f3707b1e6a7ec3d470f4afc8d7962 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 5 Oct 2018 12:16:12 +0200 Subject: [PATCH 508/733] This bit of code has been missing for unknown reasons. Was not covered by git control apparently.... --- quantipy/core/rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index 6ab9bc934..cc9d44faa 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -622,7 +622,8 @@ def verify_test_value(value): if len(value)==1: value = set(value) else: - value = set([int(i) for i in list(value[1:-1].split(','))]) + value = set([int(i) if i.isdigit() else i + for i in list(value[1:-1].split(','))]) value = cols.intersection(value) if not value: value = '' From ac397f84c53136a952fcec7a20a6cb07365c302d Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 5 Oct 2018 16:00:42 +0200 Subject: [PATCH 509/733] empty result on an array summary stat view now corrected --- quantipy/core/quantify/engine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 58d551e7e..86756ff97 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -958,7 +958,8 @@ def _empty_result(self): elif self.ydef is not None and len(self.ydef) == 0: ydim = 2 else: - ydim = len(self.ydef) + 1 + ydim = len(self.ydef) + if not self.type == 'array': ydim += 1 else: if self.xdef is not None: if len(self.xdef) == 0: From 97cdce60070119f3441a0f3dfc268fb283fc392b Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Mon, 8 Oct 2018 10:01:29 +0200 Subject: [PATCH 510/733] checkpoint --- quantipy/core/dataset.py | 69 +++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index d0ee33c12..a308667da 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3059,6 +3059,7 @@ def create_set(self, setname='new_set', based_on='data file', included=None, return None + @modify(to_list=['logic']) def add_filter_var(self, name, logic, overwrite=False): """ Create filter-var, that allows index slicing using ``manifest_filter`` @@ -3068,8 +3069,21 @@ def add_filter_var(self, name, logic, overwrite=False): name: str Name and label of the new filter-variable, which gets also listed in DataSet.filters - logic: complex logic + logic: complex logic/ str, list of complex logic/ str Logic to keep cases. + Complex logic should be provided in form of: + ``` + { + 'label': 'any text', + 'logic': {var: keys} / intersection/ .... + } + + If a str (column-name) is provided, automatically a logic is + created that keeps all cases which are not empty for this column. + If logic is a list, each included list-item becomes a category of + the new filter-variable and all cases are kept that satify all + conditions (intersection) + overwrite: bool, default False Overwrite an already existing filter-variable. """ @@ -3081,14 +3095,34 @@ def add_filter_var(self, name, logic, overwrite=False): elif not overwrite: msg = "Cannot add filter-variable '{}', it's already included." raise ValueError(msg.format(name)) - self.add_meta(name, 'single', name, [(1, 'active')]) - self[self.take(logic), name] = 1 + else: + self.drop(name) + if self._verbose_infos: + print 'Overwriting {}'.format(name) + values = [(0, 'keep', None)] + for x, l in enumerate(logic, 1): + if isinstance(l, basestring): + if not l in self: + raise KeyError("{} is not included in Dataset".format(l)) + val = (x, '{} not empty'.format(l), {l: not_count(0)}) + elif isinstance(l, dict): + if not ('label' in l and 'logic' in l): + raise KeyError("Filter logic must contain 'label' and 'logic'") + val = (x, l['label'], l['logic']) + else: + raise TypeError('Included logic must be (list of) str or dict.') + values.append(val) + + self.add_meta(name, 'delimited set', name, [(x, y) for x, y, z in values]) + self.recode(name, {x: z for x, y, z in values[1:]}) + self.recode(name, {0: {name: has_count(len(values)-1)}}, append=True) self.filters.append(name) if not 'filters' in self._meta['info']: self._meta['info']['filters'] = [] self._meta['info']['filters'].append(name) return None + @modify(to_list=['logic']) def extend_filter_var(self, name, logic, suffix='ext'): """ Extend logic of an existing filter-variable. @@ -3097,7 +3131,7 @@ def extend_filter_var(self, name, logic, suffix='ext'): ---------- name: str Name of the existing filter variable. - logic: complex logic + logic: (list of) complex logic/ str Additional logic to keep cases (intersection with existing logic). suffix: str Addition to the filter-name to create a new filter. If it is None @@ -3107,12 +3141,31 @@ def extend_filter_var(self, name, logic, suffix='ext'): raise KeyError('{} is no valid filter-variable.'.format(name)) if suffix: f_name = '{}_{}'.format(name, suffix) - overwrite = False + if f_name in self: + msg = "Please change suffix: '{}' is already in dataset." + raise KeyError(msg.format(f_name)) + self.copy(name, suffix) + self._meta['info']['filters'].append(f_name) + else: f_name = name - overwrite = True - f_logic = intersection([{name: 1}, logic]) - self.add_filter_var(f_name, f_logic, overwrite) + self.uncode(f_name, {0: {f_name: 0}}) + values = [] + for x, l in enumerate(logic, max(self.codes(f_name))+1): + if isinstance(l, basestring): + if not l in self: + raise KeyError("{} is not included in Dataset".format(l)) + val = (x, '{} not empty'.format(l), {l: not_count(0)}) + elif isinstance(l, dict): + if not ('label' in l and 'logic' in l): + raise KeyError("Filter logic must contain 'label' and 'logic'") + val = (x, l['label'], l['logic']) + else: + raise TypeError('Included logic must be (list of) str or dict.') + values.append(val) + self.extend_values(f_name, values) + self.recode(f_name, {x: z for x, y, z in values}, append=True) + self.recode(f_name, {0: {f_name: has_count(len(self.codes(f_name))-1)}}, append=True) return None def manifest_filter(self, name): From bbe49c33a829aba8c56754219a9e8b205fe0ccd1 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 8 Oct 2018 13:13:47 +0200 Subject: [PATCH 511/733] checkpoint --- quantipy/sandbox/sandbox.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index b75512d68..afe1e6900 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -681,7 +681,7 @@ def remove(self, chains, folder=None, inplace=True): return cm - def cut(self, values, ci=None): + def cut(self, values, ci=None, tests=True): """ Isolate selected axis values in the ``Chain.dataframe``. @@ -690,7 +690,7 @@ def cut(self, values, ci=None): values : (list of) str The string must indicate the raw (i.e. the unpainted) second level axis value, e.g. ``'mean'``, ``'net_1'``, etc. - ci : {'counts', 'c%', None}, default None + cell_items : {'counts', 'c%', None}, default None The cell item version to target if multiple frequency representations are present. @@ -703,7 +703,8 @@ def cut(self, values, ci=None): values[values.index('cbase')] = 'All' for c in self.chains: if c.sig_test_letters: c._remove_letter_header() - idxs, names, order = c._view_idxs(values, names=True, ci=ci) + idxs, names, order = c._view_idxs( + values, keep_tests=tests, names=True, ci=ci) idxs = [i for _, i in sorted(zip(order, idxs))] names = [n for _, n in sorted(zip(order, names))] if c.ci_count > 1: c._non_grouped_axis() @@ -749,8 +750,14 @@ def join(self, x_label='auto', y_label='auto', drop=True): chains = self.chains totalmul = len(chains[0]._frame.columns.get_level_values(0).tolist()) concat_dfs = [] + new_labels = [] for c in chains: + df = c.dataframe + + # GET FORMER FIRST LEVEL LABELS + new_labels.append(df.index.get_level_values(0).values.tolist()[0]) + df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) df.index.set_levels(levels=[x_label], level=0, inplace=True) if not c.array_style == 0: @@ -759,6 +766,11 @@ def join(self, x_label='auto', y_label='auto', drop=True): df.columns.set_levels(levels=[y_label]*totalmul, level=0, inplace=True) concat_dfs.append(df) new_df = pd.concat(concat_dfs, axis=0) + + # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) + new_df.index.set_levels(levels=new_labels, level=1, inplace=True) + new_df.index.set_labels(labels=range(0, len(new_labels)), level=1, inplace=True) + self.chains[0]._frame = new_df self.reorder([0]) self.chains[0]._custom_views = custom_views @@ -2282,7 +2294,7 @@ def _add_contents(self, parts): stat=self._stat(parts), siglevel=self._siglevel(parts)) - def _view_idxs(self, view_tags, ignore_tests=True, names=False, ci=None): + def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): """ """ if not isinstance(view_tags, list): view_tags = [view_tags] @@ -2304,7 +2316,7 @@ def _view_idxs(self, view_tags, ignore_tests=True, names=False, ci=None): else: rows.append(r) invalids = [] - if ignore_tests: + if not keep_tests: invalids.extend(['is_propstest', 'is_meanstest']) if ci == 'counts': invalids.append('is_c_pct') @@ -2313,6 +2325,7 @@ def _view_idxs(self, view_tags, ignore_tests=True, names=False, ci=None): idxs = [] names = [] order = [] + for i, row in enumerate(rows): if any([invalid in row[1] for invalid in invalids]): continue if row[0] in view_tags: From 1589e1692dc38e5050b5166ae21aeb2da98f8024 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Tue, 9 Oct 2018 11:01:06 +0200 Subject: [PATCH 512/733] bigfix in _clean_data_file_list --- quantipy/core/dataset.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 533abea4d..7356baae4 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2281,7 +2281,7 @@ def _clean_datafile_set(self): Drop references from ['sets']['data file']['items'] if they do not exist in the ``DataSet`` columns or masks definitions. """ - file_list = list(set(self._meta['sets']['data file']['items'])) + file_list = self._meta['sets']['data file']['items'] for item in file_list[:]: collection = item.split('@')[0] variable = item.split('@')[1] @@ -2298,7 +2298,10 @@ def _clean_datafile_set(self): file_list[idx] = parent while item in file_list: file_list.remove(item) - self._meta['sets']['data file']['items'] = file_list + f_list = [] + for item in file_list: + if not item in f_list: f_list.append(item) + self._meta['sets']['data file']['items'] = f_list return None def _fix_varnames(self): From d9cb80f3dc00672d8af1c69cfeb7723038c9c232 Mon Sep 17 00:00:00 2001 From: "YG\\KMue" Date: Wed, 10 Oct 2018 10:20:28 +0200 Subject: [PATCH 513/733] fix check for various sig levels --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index afd4de181..9ec568232 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1754,7 +1754,7 @@ def sig_levels(self): for m in zip(tests, levels): l = '.{}'.format(m[1]) t = m[0] - if m in sig_levels: + if t in sig_levels: sig_levels[t].append(l) else: sig_levels[t] = [l] From d3bd2c65079a0a3dc84a5438e2da84537116fe71 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 11 Oct 2018 09:44:47 +0200 Subject: [PATCH 514/733] checkpoint --- quantipy/sandbox/sandbox.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index e497f450a..2ec1c9490 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -749,27 +749,44 @@ def join(self, x_label='auto', y_label='auto', drop=True): self.unfold() chains = self.chains totalmul = len(chains[0]._frame.columns.get_level_values(0).tolist()) + # elementmul = len(chains[0].describe()) - 1 concat_dfs = [] - new_labels = [] + # new_labels = [] for c in chains: - + new_label = [] + c._frame = c._apply_letter_header(c._frame) + df = c.dataframe # GET FORMER FIRST LEVEL LABELS - new_labels.append(df.index.get_level_values(0).values.tolist()[0]) + title = [x_label] + new_label.append(df.index.get_level_values(0).values.tolist()[0]) + new_label.extend((len(c.describe()) - 1) * ['']) + names = ['Question', 'Values'] + join_idx = pd.MultiIndex.from_product([title, new_label], names=names) df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) - df.index.set_levels(levels=[x_label], level=0, inplace=True) + + + # df.index.set_levels(levels=[x_label], level=0, inplace=True) + if not c.array_style == 0: custom_views.extend(c._views_per_rows()) else: df.columns.set_levels(levels=[y_label]*totalmul, level=0, inplace=True) + + df.index = join_idx + + # # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) + # df.index.set_labels(labels=range(0, len(new_label)), level=1, inplace=True) + # df.index.set_levels(levels=new_label, level=1, inplace=True) + + concat_dfs.append(df) new_df = pd.concat(concat_dfs, axis=0) - - # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) - new_df.index.set_levels(levels=new_labels, level=1, inplace=True) - new_df.index.set_labels(labels=range(0, len(new_labels)), level=1, inplace=True) + # # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) + # new_df.index.set_levels(levels=new_labels, level=1, inplace=True) + # new_df.index.set_labels(labels=range(0, len(new_labels)), level=1, inplace=True) self.chains[0]._frame = new_df self.reorder([0]) @@ -2248,7 +2265,7 @@ def _valid_views(self, flat=False): if sub_v in valid: new_v.append(sub_v) if isinstance(v, tuple): - new_v = tuple(new_v) + new_v = list(new_v) if new_v: if len(new_v) == 1: new_v = new_v[0] if not flat: From a1fa3bde6a20d8558049e64e109391df18b141da Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 11 Oct 2018 12:00:35 +0200 Subject: [PATCH 515/733] label enhancements for join() --- quantipy/sandbox/sandbox.py | 56 +++++++++++++------------------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 2ec1c9490..ce42d47ee 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -722,75 +722,57 @@ def cut(self, values, ci=None, tests=True): c.edited = True return None - def join(self, x_label='auto', y_label='auto', drop=True): + def join(self, x_title='auto', y_title='auto', joined_index=True, + show_view_names=False): """ Join **all** ``qp.Chain```elements, concatenating along a merged x-axis. Parameters ---------- - x_label : {str, 'auto'}, default 'auto' + x_title : {str, 'auto'}, default 'auto' A new text label for the merged x-axis. - y_label : {str, 'auto'}, default 'auto' + y_title : {str, 'auto'}, default 'auto' A new text label for the merged y-axis if multiple array summaries are the input. - drop : bool, default False - By default, the original ``qp.Chain`` elements will get removed from - the resulting ``qp.ChainManager`` structure. Returns ------- None """ custom_views = [] - if x_label == 'auto': - x_label = ', '.join(c._x_keys[0] for c in self.chains) - if y_label == 'auto': - pass + if x_title == 'auto': x_title = 'Summary' + if y_title == 'auto': pass self.unfold() chains = self.chains totalmul = len(chains[0]._frame.columns.get_level_values(0).tolist()) - # elementmul = len(chains[0].describe()) - 1 concat_dfs = [] - # new_labels = [] for c in chains: new_label = [] - c._frame = c._apply_letter_header(c._frame) - + if c.sig_test_letters: c._frame = c._apply_letter_header(c._frame) df = c.dataframe - # GET FORMER FIRST LEVEL LABELS - title = [x_label] + # create a joined axis (non-summary join) new_label.append(df.index.get_level_values(0).values.tolist()[0]) new_label.extend((len(c.describe()) - 1) * ['']) names = ['Question', 'Values'] - join_idx = pd.MultiIndex.from_product([title, new_label], names=names) + join_idx = pd.MultiIndex.from_product([[x_title], new_label], names=names) + df.index = join_idx - df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) - - # df.index.set_levels(levels=[x_label], level=0, inplace=True) - + df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) + if not c.array_style == 0: custom_views.extend(c._views_per_rows()) else: - df.columns.set_levels(levels=[y_label]*totalmul, level=0, inplace=True) - - df.index = join_idx - - # # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) - # df.index.set_labels(labels=range(0, len(new_label)), level=1, inplace=True) - # df.index.set_levels(levels=new_label, level=1, inplace=True) + df.columns.set_levels(levels=[y_title]*totalmul, level=0, inplace=True) - concat_dfs.append(df) new_df = pd.concat(concat_dfs, axis=0) - # # UPDATE LEVEL 1 WITH FORMER FIRST LEVEL (0) - # new_df.index.set_levels(levels=new_labels, level=1, inplace=True) - # new_df.index.set_labels(labels=range(0, len(new_labels)), level=1, inplace=True) - + self.chains[0]._frame = new_df self.reorder([0]) self.chains[0]._custom_views = custom_views + return None def reorder(self, order, folder=None, inplace=True): @@ -2328,9 +2310,9 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): elif 'is_c_pct' in r[1] and is_code: rows.append(('c%', r[1])) elif 'is_propstest' in r[1]: - rows.append(('propstest', r[1])) + rows.append((r[0], r[1])) elif 'is_meanstest' in r[1]: - rows.append(('meanstest', r[1])) + rows.append((r[0], r[1])) else: rows.append(r) invalids = [] @@ -2343,9 +2325,9 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): idxs = [] names = [] order = [] - for i, row in enumerate(rows): - if any([invalid in row[1] for invalid in invalids]): continue + if any([invalid in row[1] for invalid in invalids]): + continue if row[0] in view_tags: order.append(view_tags.index(row[0])) idxs.append(i) From cadcc20dd2fff4715389f41cf8f512547d2491b8 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 12 Oct 2018 12:57:42 +0200 Subject: [PATCH 516/733] Adding more cut() functionality for Array summaries --- quantipy/sandbox/sandbox.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index ce42d47ee..86a09705c 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -709,7 +709,9 @@ def cut(self, values, ci=None, tests=True): names = [n for _, n in sorted(zip(order, names))] if c.ci_count > 1: c._non_grouped_axis() if c.array_style == 0: - c._frame = c._frame.iloc[:, idxs] + c._fill_cells() + start, repeat = c._row_pattern(ci) + c._frame = c._frame.iloc[start::repeat, idxs] else: c._frame = c._frame.iloc[idxs, :] c.index = c._slice_edited_index(c.index, idxs) @@ -2119,6 +2121,13 @@ def _describe(cell_defs, row_id): description = _describe(self.contents, None) return description + def _fill_cells(self): + """ + """ + self._frame = self._frame.fillna(method='ffill') + return None + + @lazy_property def _counts_first(self): for v in self.views: @@ -2294,6 +2303,17 @@ def _add_contents(self, parts): stat=self._stat(parts), siglevel=self._siglevel(parts)) + def _row_pattern(self, target_ci): + """ + """ + cisplit = self.cell_items.split('_') + if target_ci == 'c%': + start = cisplit.index('colpct') + elif target_ci == 'counts': + start = cisplit.index('counts') + repeat = self.ci_count + return (start, repeat) + def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): """ """ @@ -2301,7 +2321,19 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): rowmeta = self.named_rowmeta nested = self.array_style == 0 if nested: - rowmeta = rowmeta[0] + if self.ci_count > 1: + # cisplit = self.cell_items.split('_') + # if ci == 'c%': + # grab_rm = cisplit.index('colpct') + # elif ci == 'counts': + # grab_rm = cisplit.index('counts') + # else: + # print 'We need to support ci=None for array summary cut()...' + # raise + + rowmeta = rowmeta[self._row_pattern(ci)[0]] + else: + rowmeta = rowmeta[0] rows = [] for r in rowmeta: is_code = str(r[0]).isdigit() From 2fdfa6257cc5d2b616343807e797e5082d593f64 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 12 Oct 2018 15:45:35 +0200 Subject: [PATCH 517/733] Correct cell item selection for array sum. when ci param is used in cut() --- quantipy/sandbox/sandbox.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 86a09705c..e5b87ba2a 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -722,6 +722,7 @@ def cut(self, values, ci=None, tests=True): else: c._views[v] = names.count(v) c.edited = True + print names return None def join(self, x_title='auto', y_title='auto', joined_index=True, @@ -2239,7 +2240,6 @@ def _views_per_rows(self): vc = colpcts else: vc = counts if ci == 'counts' else colpcts - metrics.append({col: vc[col] for col in range(0, dims[1])}) return metrics @@ -2322,6 +2322,7 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): nested = self.array_style == 0 if nested: if self.ci_count > 1: + # cisplit = self.cell_items.split('_') # if ci == 'c%': # grab_rm = cisplit.index('colpct') @@ -2330,8 +2331,8 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): # else: # print 'We need to support ci=None for array summary cut()...' # raise - - rowmeta = rowmeta[self._row_pattern(ci)[0]] + rp_idx = self._row_pattern(ci)[0] + rowmeta = rowmeta[rp_idx] else: rowmeta = rowmeta[0] rows = [] @@ -2363,8 +2364,8 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): if row[0] in view_tags: order.append(view_tags.index(row[0])) idxs.append(i) - if nested: - names.append(self._views_per_rows()[i][i]) + if nested: + names.append(self._views_per_rows()[rp_idx][i]) else: names.append(self._views_per_rows()[i]) return (idxs, order) if not names else (idxs, names, order) From db66210c2ce9594a5a8ab6b1583abb89da746c73 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 12 Oct 2018 16:12:51 +0200 Subject: [PATCH 518/733] change to cell item detection, old code left in, commented --- quantipy/sandbox/sandbox.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index e5b87ba2a..3fd855eb7 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1972,13 +1972,22 @@ def cell_items(self): if self.views: compl_views = [v for v in self.views if ']*:' in v] if not compl_views: - c = any(v.split('|')[-1] == 'counts' for v in self.views) - col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) - row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) + + # c = any(v.split('|')[-1] == 'counts' for v in self.views) + # col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) + # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) + + c = any(v.split('|')[3] == '' for v in self.views) + col_pct = any(v.split('|')[3] == 'y' for v in self.views) + row_pct = any(v.split('|')[3] == 'x' for v in self.views) + + # print c + # print col_pct + # print row_pct else: c = any(v.split('|')[3] == '' for v in compl_views) col_pct = any(v.split('|')[3] == 'y' for v in compl_views) - row_pct = any(v.split('|')[3] == 'x' for v in self.views) + row_pct = any(v.split('|')[3] == 'x' for v in compl_views) c_colpct = c and col_pct c_rowpct = c and row_pct c_colrow_pct = c_colpct and c_rowpct From e177a965978ea431c7c34240cdb074b8938cb611 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 12 Oct 2018 16:13:42 +0200 Subject: [PATCH 519/733] removin prints --- quantipy/sandbox/sandbox.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 3fd855eb7..92bd8b424 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1972,18 +1972,12 @@ def cell_items(self): if self.views: compl_views = [v for v in self.views if ']*:' in v] if not compl_views: - # c = any(v.split('|')[-1] == 'counts' for v in self.views) # col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) - c = any(v.split('|')[3] == '' for v in self.views) col_pct = any(v.split('|')[3] == 'y' for v in self.views) row_pct = any(v.split('|')[3] == 'x' for v in self.views) - - # print c - # print col_pct - # print row_pct else: c = any(v.split('|')[3] == '' for v in compl_views) col_pct = any(v.split('|')[3] == 'y' for v in compl_views) From d58e68e56fb114317c26ca49bd6c8dfbef048517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 15 Oct 2018 13:33:48 +0200 Subject: [PATCH 520/733] gitignore und setup cfg for other pytest version --- .gitignore | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6cb8f1bc0..689caf355 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,6 @@ docs/API/_templates/ *.egg .eggs/ .cache +.pytest_cache/ build diff --git a/setup.cfg b/setup.cfg index 0bbc916eb..5aea674ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,4 @@ test=pytest [tool:pytest] norecursedirs=*egg .eggs .idea data debug #addopts=--cov quantipy +filterwarnings = ignore::DeprecationWarning \ No newline at end of file From 5607fc410d57353629ea65776cb21e563011b053 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 15 Oct 2018 16:22:04 +0200 Subject: [PATCH 521/733] cleanups and some other minor improvents to cut()/join() methods --- quantipy/sandbox/sandbox.py | 74 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 92bd8b424..2048b3f1a 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -681,7 +681,7 @@ def remove(self, chains, folder=None, inplace=True): return cm - def cut(self, values, ci=None, tests=True): + def cut(self, values, ci=None, base=False, tests=False): """ Isolate selected axis values in the ``Chain.dataframe``. @@ -721,61 +721,60 @@ def cut(self, values, ci=None, tests=True): del c._views[v] else: c._views[v] = names.count(v) + if not tests: c.sig_test_letters = None c.edited = True - print names + return None - def join(self, x_title='auto', y_title='auto', joined_index=True, - show_view_names=False): + def join(self, title='Summary'): """ - Join **all** ``qp.Chain```elements, concatenating along a merged x-axis. + Join **all** ``qp.Chain```elements, concatenating along the matching axis. Parameters ---------- - x_title : {str, 'auto'}, default 'auto' - A new text label for the merged x-axis. - y_title : {str, 'auto'}, default 'auto' - A new text label for the merged y-axis if multiple array summaries - are the input. - + title : {str, 'auto'}, default 'auto' + The outer axis label text that ... + joint_axis : bool, default True + Text + Returns ------- None """ custom_views = [] - if x_title == 'auto': x_title = 'Summary' - if y_title == 'auto': pass self.unfold() chains = self.chains totalmul = len(chains[0]._frame.columns.get_level_values(0).tolist()) concat_dfs = [] + new_labels = [] for c in chains: new_label = [] if c.sig_test_letters: c._frame = c._apply_letter_header(c._frame) df = c.dataframe - # create a joined axis (non-summary join) - new_label.append(df.index.get_level_values(0).values.tolist()[0]) - new_label.extend((len(c.describe()) - 1) * ['']) + if not c.array_style == 0: + new_label.append(df.index.get_level_values(0).values.tolist()[0]) + new_label.extend((len(c.describe()) - 1) * ['']) + else: + new_label.extend(df.index.get_level_values(1).values.tolist()) names = ['Question', 'Values'] - join_idx = pd.MultiIndex.from_product([[x_title], new_label], names=names) + join_idx = pd.MultiIndex.from_product([[title], new_label], names=names) df.index = join_idx - df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) if not c.array_style == 0: custom_views.extend(c._views_per_rows()) else: - df.columns.set_levels(levels=[y_title]*totalmul, level=0, inplace=True) - + df.columns.set_levels(levels=[title]*totalmul, level=0, inplace=True) + concat_dfs.append(df) - new_df = pd.concat(concat_dfs, axis=0) + + new_df = pd.concat(concat_dfs, axis=0, join='inner') self.chains[0]._frame = new_df self.reorder([0]) self.chains[0]._custom_views = custom_views - return None def reorder(self, order, folder=None, inplace=True): @@ -1971,17 +1970,26 @@ def sig_levels(self): def cell_items(self): if self.views: compl_views = [v for v in self.views if ']*:' in v] - if not compl_views: - # c = any(v.split('|')[-1] == 'counts' for v in self.views) - # col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) - # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) - c = any(v.split('|')[3] == '' for v in self.views) - col_pct = any(v.split('|')[3] == 'y' for v in self.views) - row_pct = any(v.split('|')[3] == 'x' for v in self.views) - else: - c = any(v.split('|')[3] == '' for v in compl_views) - col_pct = any(v.split('|')[3] == 'y' for v in compl_views) - row_pct = any(v.split('|')[3] == 'x' for v in compl_views) + check_views = compl_views or self.views + # if not compl_views: + # # c = any(v.split('|')[-1] == 'counts' for v in self.views) + # # col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) + # # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) + non_freqs = ('d.', 't.') + c = any(v.split('|')[3] == '' and + not v.split('|')[1].startswith(non_freqs) + for v in check_views) + col_pct = any(v.split('|')[3] == 'y' and + not v.split('|')[1].startswith(non_freqs) + for v in check_views) + row_pct = any(v.split('|')[3] == 'x' and + not v.split('|')[1].startswith(non_freqs) + for v in check_views) + # else: + # c = any(v.split('|')[3] == '' for v in compl_views) + # col_pct = any(v.split('|')[3] == 'y' for v in compl_views) + # row_pct = any(v.split('|')[3] == 'x' for v in compl_views) + c_colpct = c and col_pct c_rowpct = c and row_pct c_colrow_pct = c_colpct and c_rowpct From 8f262653eaaf52774de2802bd963aa52d2590151 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 16 Oct 2018 09:56:08 +0200 Subject: [PATCH 522/733] checkpoint --- quantipy/sandbox/sandbox.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 2048b3f1a..8d33a1270 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -701,10 +701,12 @@ def cut(self, values, ci=None, base=False, tests=False): if not isinstance(values, list): values = [values] if 'cbase' in values: values[values.index('cbase')] = 'All' + if base and not 'All' in values: + values = ['All'] + values for c in self.chains: if c.sig_test_letters: c._remove_letter_header() idxs, names, order = c._view_idxs( - values, keep_tests=tests, names=True, ci=ci) + values, keep_tests=tests, keep_bases=base, names=True, ci=ci) idxs = [i for _, i in sorted(zip(order, idxs))] names = [n for _, n in sorted(zip(order, names))] if c.ci_count > 1: c._non_grouped_axis() @@ -732,11 +734,9 @@ def join(self, title='Summary'): Parameters ---------- - title : {str, 'auto'}, default 'auto' - The outer axis label text that ... - joint_axis : bool, default True - Text - + title : {str, 'auto'}, default 'Summary' + The new title for the joined axis' index representation. + Returns ------- None @@ -2325,7 +2325,7 @@ def _row_pattern(self, target_ci): repeat = self.ci_count return (start, repeat) - def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): + def _view_idxs(self, view_tags, keep_tests=True, keep_bases=True, names=False, ci=None): """ """ if not isinstance(view_tags, list): view_tags = [view_tags] @@ -2333,15 +2333,6 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): nested = self.array_style == 0 if nested: if self.ci_count > 1: - - # cisplit = self.cell_items.split('_') - # if ci == 'c%': - # grab_rm = cisplit.index('colpct') - # elif ci == 'counts': - # grab_rm = cisplit.index('counts') - # else: - # print 'We need to support ci=None for array summary cut()...' - # raise rp_idx = self._row_pattern(ci)[0] rowmeta = rowmeta[rp_idx] else: @@ -2371,7 +2362,7 @@ def _view_idxs(self, view_tags, keep_tests=True, names=False, ci=None): order = [] for i, row in enumerate(rows): if any([invalid in row[1] for invalid in invalids]): - continue + if not (row[0] == 'All' and keep_bases): continue if row[0] in view_tags: order.append(view_tags.index(row[0])) idxs.append(i) From b3cde4a089cc13dc1ca3eb28e55da9c00daccdd2 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 16 Oct 2018 11:28:27 +0200 Subject: [PATCH 523/733] correct local name sort_on_stat replaced --- quantipy/core/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index cc9d44faa..f56e705b3 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -173,7 +173,7 @@ def get_slicer(self): sort_on_net = False if isinstance(sort_on, (str, unicode)): - sort_on_mean = sort_on in [ + sort_on_stat = sort_on in [ 'median', 'stddev', 'sem', 'max', 'min', 'mean', 'upper_q', 'lower_q'] sort_on_net = sort_on.startswith('net') From 250bcb5c5877a80044398862c73a11f1b759e60e Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 16 Oct 2018 11:46:59 +0200 Subject: [PATCH 524/733] correcting cell_items() for cut() Chains that also show bases --- quantipy/sandbox/sandbox.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 8d33a1270..e5a4372cb 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1977,13 +1977,16 @@ def cell_items(self): # # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) non_freqs = ('d.', 't.') c = any(v.split('|')[3] == '' and - not v.split('|')[1].startswith(non_freqs) + not v.split('|')[1].startswith(non_freqs) and + not v.split('|')[-1] == 'cbase' for v in check_views) col_pct = any(v.split('|')[3] == 'y' and - not v.split('|')[1].startswith(non_freqs) + not v.split('|')[1].startswith(non_freqs) and + not v.split('|')[-1] == 'cbase' for v in check_views) row_pct = any(v.split('|')[3] == 'x' and - not v.split('|')[1].startswith(non_freqs) + not v.split('|')[1].startswith(non_freqs) and + not v.split('|')[-1] == 'cbase' for v in check_views) # else: # c = any(v.split('|')[3] == '' for v in compl_views) From c84d2eda3fac3e1f05ba7bd326fe74057e78017f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 17 Oct 2018 10:50:14 +0200 Subject: [PATCH 525/733] gitignore and ignore test warnings --- .gitignore | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index db0a65d0d..21c7716d8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ docs/API/_templates/ *.egg .eggs/ .cache +.pytest_cache/ build diff --git a/setup.cfg b/setup.cfg index 0bbc916eb..5aea674ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,4 @@ test=pytest [tool:pytest] norecursedirs=*egg .eggs .idea data debug #addopts=--cov quantipy +filterwarnings = ignore::DeprecationWarning \ No newline at end of file From 4fa71c98ef7f2a57e6fb1fe6dc610fa746c867d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 17 Oct 2018 10:51:01 +0200 Subject: [PATCH 526/733] fix bug for delimited sets in _paint_structure --- quantipy/sandbox/sandbox.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index dfd3439b8..a16f8e31f 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2953,7 +2953,7 @@ def _paint_structure(self, text_key=None, sep=None, na_rep=None): pattern = r'\, (?=\W|$)' for column in self.structure.columns: - if not column in self._meta['columns']: return None + if not column in self._meta['columns']: continue meta = self._meta['columns'][column] @@ -2975,20 +2975,23 @@ def _paint_structure(self, text_key=None, sep=None, na_rep=None): for item in values } series = self.structure[column] - series = (series.str.split(';') - .apply(pd.Series, 1) - .stack(dropna=False) - .map(value_mapper.get) #, na_action='ignore') - .unstack()) - first = series[series.columns[0]] - rest = [series[c] for c in series.columns[1:]] - self.structure[column] = ( - first - .str.cat(rest, sep=', ', na_rep='') - .str.slice(0, -2) - .replace(to_replace=pattern, value='', regex=True) - .replace(to_replace='', value=na_rep) - ) + try: + series = (series.str.split(';') + .apply(pd.Series, 1) + .stack(dropna=False) + .map(value_mapper.get) #, na_action='ignore') + .unstack()) + first = series[series.columns[0]] + rest = [series[c] for c in series.columns[1:]] + self.structure[column] = ( + first + .str.cat(rest, sep=', ', na_rep='') + .str.slice(0, -2) + .replace(to_replace=pattern, value='', regex=True) + .replace(to_replace='', value=na_rep) + ) + except AttributeError: + continue else: value_mapper = { item['value']: item['text'][text_key] From 24c50d714c3001aaf028e21e3e47b81f80edfaa7 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 17 Oct 2018 12:21:49 +0200 Subject: [PATCH 527/733] checkpoint --- quantipy/sandbox/sandbox.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index e5a4372cb..0682278a6 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -680,7 +680,6 @@ def remove(self, chains, folder=None, inplace=True): else: return cm - def cut(self, values, ci=None, base=False, tests=False): """ Isolate selected axis values in the ``Chain.dataframe``. @@ -690,9 +689,13 @@ def cut(self, values, ci=None, base=False, tests=False): values : (list of) str The string must indicate the raw (i.e. the unpainted) second level axis value, e.g. ``'mean'``, ``'net_1'``, etc. - cell_items : {'counts', 'c%', None}, default None + ci : {'counts', 'c%', None}, default None The cell item version to target if multiple frequency representations are present. + base : bool, default False + Controls keeping any existing base view aggregations. + tests : bool, default False + Controls keeping any existing significance test view aggregations. Returns ------- @@ -723,7 +726,10 @@ def cut(self, values, ci=None, base=False, tests=False): del c._views[v] else: c._views[v] = names.count(v) - if not tests: c.sig_test_letters = None + if not tests: + c.sig_test_letters = None + else: + c._frame = c._apply_letter_header(c._frame) c.edited = True return None @@ -749,7 +755,9 @@ def join(self, title='Summary'): new_labels = [] for c in chains: new_label = [] - if c.sig_test_letters: c._frame = c._apply_letter_header(c._frame) + if c.sig_test_letters: + c._remove_letter_header() + c._frame = c._apply_letter_header(c._frame) df = c.dataframe if not c.array_style == 0: @@ -1971,10 +1979,6 @@ def cell_items(self): if self.views: compl_views = [v for v in self.views if ']*:' in v] check_views = compl_views or self.views - # if not compl_views: - # # c = any(v.split('|')[-1] == 'counts' for v in self.views) - # # col_pct = any(v.split('|')[-1] == 'c%' for v in self.views) - # # row_pct = any(v.split('|')[-1] == 'r%' for v in self.views) non_freqs = ('d.', 't.') c = any(v.split('|')[3] == '' and not v.split('|')[1].startswith(non_freqs) and @@ -1988,15 +1992,9 @@ def cell_items(self): not v.split('|')[1].startswith(non_freqs) and not v.split('|')[-1] == 'cbase' for v in check_views) - # else: - # c = any(v.split('|')[3] == '' for v in compl_views) - # col_pct = any(v.split('|')[3] == 'y' for v in compl_views) - # row_pct = any(v.split('|')[3] == 'x' for v in compl_views) - c_colpct = c and col_pct c_rowpct = c and row_pct c_colrow_pct = c_colpct and c_rowpct - single_ci = not (c_colrow_pct or c_colpct or c_rowpct) if single_ci: if c: From e60985b46b53faf0905c6252b6b5be37c178c38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 17 Oct 2018 13:50:44 +0200 Subject: [PATCH 528/733] add property to filters --- quantipy/core/batch.py | 1 - quantipy/core/dataset.py | 136 +++++++++++++++++++-------------------- 2 files changed, 66 insertions(+), 71 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index e3ce3c260..0a50b2ad6 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -108,7 +108,6 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.valid_tks = dataset.valid_tks self.text_key = dataset.text_key self.sample_size = None - self.filters = dataset.filters # global dataset filters self._verbose_errors = dataset._verbose_errors self._verbose_infos = dataset._verbose_infos diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index c6f92db4b..b3e2309dc 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -48,13 +48,16 @@ from itertools import product, chain from collections import OrderedDict, Counter -VALID_TKS = ['en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE', 'fr-FR', - 'ar-AR', 'es-ES', 'it-IT'] -VAR_SUFFIXES = ['_rc', '_net', ' (categories', ' (NET', '_rec'] +VALID_TKS = [ + 'en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE', 'fr-FR', 'ar-AR', + 'es-ES', 'it-IT'] -BLACKLIST_VARIABLES = ['batches', 'columns', 'info', 'items', 'lib', 'masks', - 'name', 'parent', 'properties', 'text', 'type', 'sets', - 'subtype', 'values'] +VAR_SUFFIXES = [ + '_rc', '_net', ' (categories', ' (NET', '_rec'] + +BLACKLIST_VARIABLES = [ + 'batches', 'columns', 'info', 'items', 'lib', 'masks', 'name', 'parent', + 'properties', 'text', 'type', 'sets', 'subtype', 'values', 'filter'] class DataSet(object): """ @@ -66,7 +69,6 @@ def __init__(self, name, dimensions_comp=True): self.path = None self.name = name self.filtered = 'no_filter' - self.filters = [] self._data = None self._meta = None self.text_key = None @@ -189,16 +191,19 @@ def strings(self): return self._get_columns('string') def created(self): - return [v for v in self.variables() if self.get_property(v, 'created')] + return self._by_property('created') + + def filters(self): + return self._by_property('recoded_filter') def _stat_view_recodes(self): - return [v for v in self.variables() if - self.get_property(v, 'recoded_stat')] + return self._by_property('recoded_stat') def _net_view_recodes(self): - return [v for v in self.variables() if - self.get_property(v, 'recoded_net')] + return self._by_property('recoded_net') + def _by_property(self, prop): + return [v for v in self.variables() if self.get_property(v, prop)] def batches(self): if 'batches' in self._meta['sets']: @@ -374,6 +379,9 @@ def _is_delimited_set_mapper(self, mapper): else: return False + def is_filter(self, var): + return True if self.get_property(var, recoded_filter) else False + def _has_missings(self, var): if self.is_array(var): var = self.sources(var)[0] return self._meta['columns'][var].get('missings', False) @@ -1089,7 +1097,6 @@ def _set_file_info(self, path_data, path_meta=None, reset=True): self.path = '/'.join(path_data.split('/')[:-1]) + '/' self.text_key = self._meta['lib'].get('default text') self.valid_tks = self._meta['lib'].get('valid text', VALID_TKS) - self.filters = self._meta['info'].get('filters', []) self._data['@1'] = np.ones(len(self._data)) self._meta['columns']['@1'] = {'type': 'int'} self._data.index = list(xrange(0, len(self._data.index))) @@ -3167,7 +3174,7 @@ def add_filter_var(self, name, logic, overwrite=False): Overwrite an already existing filter-variable. """ if name in self: - if overwrite and not name in self.filters: + if overwrite and not self.is_filter(name): msg = "Cannot add filter-variable '{}', a non-filter" msg +=" variable is already included" raise ValueError(msg.format(name)) @@ -3179,26 +3186,11 @@ def add_filter_var(self, name, logic, overwrite=False): if self._verbose_infos: print 'Overwriting {}'.format(name) values = [(0, 'keep', None)] - for x, l in enumerate(logic, 1): - if isinstance(l, basestring): - if not l in self: - raise KeyError("{} is not included in Dataset".format(l)) - val = (x, '{} not empty'.format(l), {l: not_count(0)}) - elif isinstance(l, dict): - if not ('label' in l and 'logic' in l): - raise KeyError("Filter logic must contain 'label' and 'logic'") - val = (x, l['label'], l['logic']) - else: - raise TypeError('Included logic must be (list of) str or dict.') - values.append(val) - + values += self._transform_filter_logics(logic, 1) self.add_meta(name, 'delimited set', name, [(x, y) for x, y, z in values]) self.recode(name, {x: z for x, y, z in values[1:]}) self.recode(name, {0: {name: has_count(len(values)-1)}}, append=True) - self.filters.append(name) - if not 'filters' in self._meta['info']: - self._meta['info']['filters'] = [] - self._meta['info']['filters'].append(name) + self._set_property(name, 'recoded_filter', True) return None @modify(to_list=['logic']) @@ -3216,7 +3208,7 @@ def extend_filter_var(self, name, logic, suffix='ext'): Addition to the filter-name to create a new filter. If it is None the existing filter-variable is overwritten. """ - if not name in self.filters: + if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) if suffix: f_name = '{}_{}'.format(name, suffix) @@ -3224,13 +3216,19 @@ def extend_filter_var(self, name, logic, suffix='ext'): msg = "Please change suffix: '{}' is already in dataset." raise KeyError(msg.format(f_name)) self.copy(name, suffix) - self._meta['info']['filters'].append(f_name) - + self._set_property(f_name, 'recoded_filter', True) else: f_name = name self.uncode(f_name, {0: {f_name: 0}}) + values = self._transform_filter_logics(logic, max(self.codes(f_name))+1) + self.extend_values(f_name, values) + self.recode(f_name, {x: z for x, y, z in values}, append=True) + self.recode(f_name, {0: {f_name: has_count(len(self.codes(f_name))-1)}}, append=True) + return None + + def _transform_filter_logics(self, logic, start): values = [] - for x, l in enumerate(logic, max(self.codes(f_name))+1): + for x, l in enumerate(logic, start): if isinstance(l, basestring): if not l in self: raise KeyError("{} is not included in Dataset".format(l)) @@ -3242,10 +3240,7 @@ def extend_filter_var(self, name, logic, suffix='ext'): else: raise TypeError('Included logic must be (list of) str or dict.') values.append(val) - self.extend_values(f_name, values) - self.recode(f_name, {x: z for x, y, z in values}, append=True) - self.recode(f_name, {0: {f_name: has_count(len(self.codes(f_name))-1)}}, append=True) - return None + return values def manifest_filter(self, name): """ @@ -3254,11 +3249,11 @@ def manifest_filter(self, name): Parameters ---------- name: str - Name of the filter_variable (valid names in self.filters). + Name of the filter_variable. """ - if not name in self.filters: + if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) - return self.take({name: 1}) + return self.take({name: 0}) # ------------------------------------------------------------------------ # extending / merging @@ -3824,8 +3819,6 @@ def remove_loop(obj, var): meta['sets']['data file']['items'] = n_items data_drop = [] for var in name: - if var in self.filters: - self.filters.remove(var) if not self.is_array(var): data_drop.append(var) remove_loop(meta, var) data.drop(data_drop, 1, inplace=True) @@ -4896,19 +4889,10 @@ def rename_properties(mapper): stat_recs = self._stat_view_recodes() all_recs = set([r for r in net_recs + stat_recs if r in mapper]) for rec in all_recs: - is_array = rec in self.masks() - if is_array: - props = self._meta['masks'][rec]['properties'] - else: - props = self._meta['columns'][rec]['properties'] - rn = props.get('recoded_net', None) - if rn: - org_ref = props['recoded_net'] - props['recoded_net'] = mapper[org_ref] - rs = props.get('recoded_stat', None) - if rs: - org_ref = props['recoded_stat'] - props['recoded_stat'] = mapper[org_ref] + rn = self.get_property(rec, 'recoded_net') + if rn: self._set_property(rec, 'recoded_net', mapper[rn]) + rs = self.get_property(rec, 'recoded_stat') + if rs: self._set_property(rec, 'recoded_stat', mapper[rs]) return None def rename_meta(meta, mapper): @@ -6105,6 +6089,7 @@ def get_property(self, name, prop_name, text_key=None): else: return None + @modify(to_list='name') @verify(variables={'name': 'both'}) def set_property(self, name, prop_name, prop_value, ignore_items=False): """ @@ -6132,19 +6117,27 @@ def set_property(self, name, prop_name, prop_value, ignore_items=False): valid_props = ['base_text', '_no_valid_items', '_no_valid_values'] if prop_name not in valid_props: raise ValueError("'prop_name' must be one of {}".format(valid_props)) + self._set_property(name, prop_name, prop_value, ignore_items) + return None + + @modify(to_list='name') + @verify(variables={'name': 'both'}) + def _set_property(self, name, prop_name, prop_value, ignore_items=False): + """ + Access and set the value of a meta object's ``properties`` collection. + + Note: This method allows the setting for any property, so it should only + be used by developers. + """ prop_update = {prop_name: prop_value} - if self.is_array(name): - if not 'properties' in self._meta['masks'][name]: - self._meta['masks'][name]['properties'] = {} - self._meta['masks'][name]['properties'].update(prop_update) - if not ignore_items: - items = self.sources(name) - for i in items: - self.set_property(i, prop_name, prop_value) - else: - if not 'properties' in self._meta['columns'][name]: - self._meta['columns'][name]['properties'] = {} - self._meta['columns'][name]['properties'].update(prop_update) + for n in name: + collection = 'masks' if self.is_array(n) else 'columns' + if not 'properties' in self._meta[collection][n]: + self._meta[collection][n]['properties'] = {} + self._meta[collection][n]['properties'].update(prop_update) + if ignore_items: continue + for s in self.sources(n): + self._set_property(s, prop_name, prop_value) return None @modify(to_list='name') @@ -6471,7 +6464,10 @@ def _clean_missing_map(self, var, missing_map): return valid_map def _set_default_missings(self, ignore=None): - excludes = ['weißnicht', 'keineangabe', 'weißnicht/keineangabe', + excludes = [ + u'weißnicht', + 'keineangabe', + u'weißnicht/keineangabe', 'keineangabe/weißnicht', 'kannmichnichterinnern', 'weißichnicht', 'nichtindeutschland'] d = self.describe() From dc6a7af349afe19c6844abd030e4473e99f8622a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 18 Oct 2018 11:37:56 +0200 Subject: [PATCH 529/733] adjustments in extend_filter_var --- quantipy/core/dataset.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index b3e2309dc..4c835098f 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -380,7 +380,7 @@ def _is_delimited_set_mapper(self, mapper): return False def is_filter(self, var): - return True if self.get_property(var, recoded_filter) else False + return True if self.get_property(var, 'recoded_filter') else False def _has_missings(self, var): if self.is_array(var): var = self.sources(var)[0] @@ -3163,7 +3163,7 @@ def add_filter_var(self, name, logic, overwrite=False): 'label': 'any text', 'logic': {var: keys} / intersection/ .... } - + ``` If a str (column-name) is provided, automatically a logic is created that keeps all cases which are not empty for this column. If logic is a list, each included list-item becomes a category of @@ -3194,7 +3194,7 @@ def add_filter_var(self, name, logic, overwrite=False): return None @modify(to_list=['logic']) - def extend_filter_var(self, name, logic, suffix='ext'): + def extend_filter_var(self, name, logic, extend_as=None): """ Extend logic of an existing filter-variable. @@ -3204,18 +3204,25 @@ def extend_filter_var(self, name, logic, suffix='ext'): Name of the existing filter variable. logic: (list of) complex logic/ str Additional logic to keep cases (intersection with existing logic). - suffix: str + Complex logic should be provided in form of: + ``` + { + 'label': 'any text', + 'logic': {var: keys} / intersection/ .... + } + ``` + extend_as: str, default None Addition to the filter-name to create a new filter. If it is None the existing filter-variable is overwritten. """ if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) - if suffix: - f_name = '{}_{}'.format(name, suffix) + if extend_as: + f_name = '{}_{}'.format(name, extend_as) if f_name in self: - msg = "Please change suffix: '{}' is already in dataset." + msg = "Please change 'extend_as': '{}' is already in dataset." raise KeyError(msg.format(f_name)) - self.copy(name, suffix) + self.copy(name, extend_as) self._set_property(f_name, 'recoded_filter', True) else: f_name = name From 141075f72ed1ef4c9dab44526efb10073d218e88 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 18 Oct 2018 11:49:07 +0200 Subject: [PATCH 530/733] checkout --- quantipy/sandbox/sandbox.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 0682278a6..049e50dee 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -777,11 +777,12 @@ def join(self, title='Summary'): df.columns.set_levels(levels=[title]*totalmul, level=0, inplace=True) concat_dfs.append(df) - - new_df = pd.concat(concat_dfs, axis=0, join='inner') - + + new_df = pd.concat(concat_dfs, axis=0, join='inner') self.chains[0]._frame = new_df self.reorder([0]) + self.rename({self.single_names[0]: title}) + self.fold() self.chains[0]._custom_views = custom_views return None From 5c0d0f1577d892a2ada6e16549c1e84aeb8073cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 18 Oct 2018 11:57:05 +0200 Subject: [PATCH 531/733] take all cases if included filter logic is empty --- quantipy/core/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 4c835098f..64271e34b 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3234,6 +3234,7 @@ def extend_filter_var(self, name, logic, extend_as=None): return None def _transform_filter_logics(self, logic, start): + if not logic: logic = ['@1'] values = [] for x, l in enumerate(logic, start): if isinstance(l, basestring): From 99bee5b71ffc47a0cd1ac889462164175bc5b584 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 18 Oct 2018 13:07:53 +0200 Subject: [PATCH 532/733] bugfix for wrong evaluation of index value in insert() --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 049e50dee..33e3ef5bc 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -488,7 +488,7 @@ def insert(self, other_cm, index=-1, safe_names=False): """ if not isinstance(other_cm, ChainManager): raise ValueError("other_cm must be a quantipy.ChainManager instance.") - if not index == -1: + if index > -1: before_c = self.__chains[:index+1] after_c = self.__chains[index+1:] new_chains = before_c + other_cm.__chains + after_c From 7ed61e2dd94d2e1aed19c197fc1e0de7bfa01ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 18 Oct 2018 13:22:08 +0200 Subject: [PATCH 533/733] reduce_filter_var --- quantipy/core/dataset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 64271e34b..38c1a7974 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3250,6 +3250,22 @@ def _transform_filter_logics(self, logic, start): values.append(val) return values + @modify(to_list=['values']) + def reduce_filter_var(self, name, values): + """ + Remove values from filter-variables and recalculate the filter. + """ + if not self.is_filter(name): + raise KeyError('{} is no valid filter-variable.'.format(name)) + if 0 in values: + raise ValueError('Cannot remove the 0-keep value from filter var') + elif len([x for x in self.codes(name) if not x in values]) <= 1: + raise ValueError('Cannot remove all values from filter var.') + self.uncode(name, {0: {name: 0}}) + self.remove_values(name, values) + self.recode(name, {0: {name: has_count(len(self.codes(name))-1)}}, append=True) + return None + def manifest_filter(self, name): """ Get index slicer from filter-variables. From 4d904771f2faf3ceb07f2a796095c235972c150a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 18 Oct 2018 16:26:18 +0200 Subject: [PATCH 534/733] checkpoint --- quantipy/core/dataset.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 38c1a7974..7afb844fe 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -891,7 +891,6 @@ def _vars_from_batch(self, batchdef, mode='batch-full'): if w: batch_vars.extend(w) return batch_vars - @modify(to_list=['text_key', 'include']) @verify(text_keys='text_key', variables={'include': 'both'}) def _from_batch(self, batch_name, include='identity', text_key=[], @@ -901,15 +900,22 @@ def _from_batch(self, batch_name, include='identity', text_key=[], # get the main batch definition to construct a dataset from... batch_def = self._meta['sets']['batches'][batch_name] # filter it if needed: - if batch_def['filter'] == 'no_filter': + f = batch_def['filter'] + if f == 'no_filter': b_ds = self.clone() + elif isinstance(f, basestring): + b_ds = self.filter(batch_name, {f: 0}) else: - b_ds = self.filter(batch_name, batch_def['filter'].values()[0]) + b_ds = self.filter(batch_name, f.values()[0]) # build the variable collection based in Batch setup & requirements: main_variables = b_ds._vars_from_batch(batch_def, 'batch-full') if additions in ['full', 'filters']: - print 'manifest_filters() needed, add filters to list...' - pass + if batch_def['additions']: + for add_batch in batch_def['additions']: + f = add_batch['filter'] + if not f == 'no_filter' and isinstance(f, basestring): + if not f in main_variables: + main_variables.append(f) if additions in ['full', 'variables']: if batch_def['additions']: for add_batch in batch_def['additions']: @@ -927,7 +933,6 @@ def _from_batch(self, batch_name, include='identity', text_key=[], self._rename_blacklist_vars() return b_ds - @modify(to_list=['text_key', 'include']) @verify(text_keys='text_key', variables={'include': 'both'}) def from_batch(self, batch_name, include='identity', text_key=[], @@ -1007,10 +1012,13 @@ def _apply_edits_rules(ds, name, b_meta): msg = 'Batch-textkey {} is not included in {}.' raise ValueError(msg.format(batch['language'], text_key)) # Create a new instance by filtering or cloning - if not batch['filter']: + f = batch['filter'] + if f == 'no_filter': b_ds = self.clone() + elif isinstance(f, basestring): + b_ds = self.filter(batch_name, {f: 0}) else: - b_ds = self.filter(batch_name, {batch['filter']: 1}) + b_ds = self.filter(batch_name, f.values()[0]) # Get a subset of variables (xks, yks, oe, weights) variables = include From 0193635625babe0e52a7309cf5af2f1f56dd5f8c Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 19 Oct 2018 10:46:12 +0200 Subject: [PATCH 535/733] typo in docstring --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 33e3ef5bc..56a6f2382 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -476,7 +476,7 @@ def insert(self, other_cm, index=-1, safe_names=False): A ChainManager instance to draw the elements from. index : int, default -1 The positional index after which new elements will be added. - Defaults to -1, i.e. elements are appended index the end. + Defaults to -1, i.e. elements are appended at the end. safe_names : bool, default False If True and any duplicated element names are found after the operation, names will be made unique (by appending '_1', '_2', '_3', From 1e88f7942eed2739e4bbbb6b4790a78bb27eae8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 19 Oct 2018 11:20:03 +0200 Subject: [PATCH 536/733] filter in batch: checkpoint --- quantipy/core/batch.py | 53 ++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 0a50b2ad6..2762f3430 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -127,9 +127,8 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.extended_yks_per_x = {} self.exclusive_yks_per_x = {} self.extended_filters_per_x = {} - self.filter = None # local batch filter - self.filter_names = [] # local batch filters - self._filter_slice = None + self.filter = None + self.filter_names = [] self.x_y_map = None self.x_filter_map = None self.y_on_y = [] @@ -182,7 +181,7 @@ def _update(self): 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', 'unwgt_counts', 'y_on_y_filter', 'y_filter_map', 'build_info', - '_filter_slice']: + ]: attr_update = {attr: self.__dict__.get(attr)} self._meta['sets']['batches'][self.name].update(attr_update) @@ -198,7 +197,7 @@ def _load_batch(self): 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', 'y_on_y_filter', 'y_filter_map', 'build_info', - '_filter_slice']: + ]: attr_load = {attr: self._meta['sets']['batches'][self.name].get(attr)} self.__dict__.update(attr_load) @@ -667,15 +666,12 @@ def add_filter(self, filter_name, filter_logic=None): None """ name = filter_name.encode('utf-8', errors='ignore') - if name in self: - if name in self._meta['info'].get('filters', []): - if filter_logic is None: - pass - elif not self.manifest_filter(name).tolist() == self.take(filter_logic).tolist(): - msg = "filter_name is already used for a filter with an other logic" - raise ValueError(msg) + if self.is_filter(name) and not filter_logic is None: + raise ValueError("'{}' is already a filter-variable. Cannot " + "apply a new logic.") else: - self.add_filter_var(filter_name, filter_logic, False) + self.add_filter_var(name, filter_logic, False) + self.filter = name if not name in self.filter_names: self.filter_names.append(name) @@ -738,19 +734,27 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, if dupes: raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): - ds = qp.DataSet('open_end') - ds.from_components(self._data, self._meta, reset=False) - if self.filter: - if filter_by: - f = intersection([{self.filter: 1}, filter_by]) - slicer = ds.take(f).tolist() + if filter_by: + if self.filter: + f_name = '{}_{}'.format(self.filter, title) + else: + f_name = title + if self.is_filter(f_name): + logic = intersection([{self.filter: 0, filter_by}]) + if not self.take(logic).index.tolist() == self.manifest_filter(f_name): + msg = "'{}' is already in use with an other logic." + raise ValueError(msg.format(f_name)) else: - slicer = self.manifest_filter(self.filter).tolist() - elif filter_by: - f = filter_by - slicer = ds.take(f).tolist() + logic = { + 'label': title, + 'logic': filter_by} + if self.filter: + self.extend_filter_var(self.filter, logic, title) + else: + self.add_filter_var(title, logic) + slicer = f_name else: - slicer = self._data.index.tolist() + slicer = self.filter if any(oe['title'] == title for oe in self.verbatims) and not overwrite: print 'Cannot include %s, as it is already included' % title return None @@ -1073,5 +1077,4 @@ def _samplesize_from_batch_filter(self): else: idx = self._data.index self.sample_size = len(idx) - self._filter_slice = idx.values.tolist() return None From a04f4e7c82f9bf65c464a226fd7b1ead01948530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 19 Oct 2018 11:30:44 +0200 Subject: [PATCH 537/733] filter in batch: checkpoint --- quantipy/core/batch.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 2762f3430..168e1bb6b 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -738,20 +738,18 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): if self.filter: f_name = '{}_{}'.format(self.filter, title) else: - f_name = title + f_name = '{}_f'.format(title) if self.is_filter(f_name): logic = intersection([{self.filter: 0, filter_by}]) if not self.take(logic).index.tolist() == self.manifest_filter(f_name): msg = "'{}' is already in use with an other logic." raise ValueError(msg.format(f_name)) else: - logic = { - 'label': title, - 'logic': filter_by} + logic = {'label': title, 'logic': filter_by} if self.filter: self.extend_filter_var(self.filter, logic, title) else: - self.add_filter_var(title, logic) + self.add_filter_var(f_name, logic) slicer = f_name else: slicer = self.filter @@ -877,7 +875,14 @@ def extend_filter(self, ext_filters): if not isinstance(variables, (list, tuple)): variables = [variables] for v in variables: - self.extended_filters_per_x.update({v: logic}) + if self.filter: + log = {'label': v, 'logic': logic} + f_name = '{}_{}'.format(self.filter, v) + self.extend_filter_var(self.filter, log, v) + else: + f_name = '{}_f'.format(v) + self.add_filter_var(f_name, log) + self.extended_filters_per_x.update({v: f_name}) self._update() return None From eb87cd0a4f18730add367cd02d845fc5afa63e68 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 19 Oct 2018 11:32:53 +0200 Subject: [PATCH 538/733] adding a Banner id column to describe() to see same banner ykeys within the ChainManager.chains collection --- quantipy/sandbox/sandbox.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 56a6f2382..69110ef62 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -903,6 +903,14 @@ def _native_stat_names(self, idxvals_list, text_key=None): native_stat_names.append(val) return native_stat_names + def _get_ykey_mapping(self): + ys = [] + letters = string.ascii_uppercase + string.ascii_lowercase + for c in self.chains: + if c._y_keys not in ys: + ys.append(c._y_keys) + return zip(ys, letters) + def describe(self, by_folder=False, show_hidden=False): """ Get a structual summary of all ``qp.Chain`` instances found in self. @@ -927,8 +935,10 @@ def describe(self, by_folder=False, show_hidden=False): names = [] array_sum = [] sources = [] + banner_ids = [] item_pos = [] hidden = [] + bannermap = self._get_ykey_mapping() for pos, chains in enumerate(self): is_folder = isinstance(chains, dict) if is_folder: @@ -949,14 +959,16 @@ def describe(self, by_folder=False, show_hidden=False): array_sum.extend([True if c.array_style > -1 else False for c in chains]) sources.extend(c.source for c in chains) - + for c in chains: + for m in bannermap: + if m[0] == c._y_keys: banner_ids.append(m[1]) else: - variables.extend([chains[0].name])#(chains[0].structure.columns.tolist()) - names.extend([chains[0].name])# for _ in xrange(chains[0].structure.shape[1])]) - # names.extend(chains[0].structure.columns.tolist()) + variables.extend([chains[0].name]) + names.extend([chains[0].name]) folders.extend(folder_name) array_sum.extend([False]) sources.extend(c.source for c in chains) + banner_ids.append(None) for c in chains: if c.hidden: hidden.append(True) @@ -968,6 +980,7 @@ def describe(self, by_folder=False, show_hidden=False): folder_items, variables, sources, + banner_ids, array_sum, hidden] df_cols = ['Position', @@ -976,6 +989,7 @@ def describe(self, by_folder=False, show_hidden=False): 'Item', 'Variable', 'Source', + 'Banner id', 'Array', 'Hidden'] df = pd.DataFrame(df_data).T From 053eae60946279bbee4983092172a89fbd3e0c6a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 19 Oct 2018 11:54:41 +0200 Subject: [PATCH 539/733] reflecting summary chains in describe() correctly --- quantipy/sandbox/sandbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 69110ef62..3a9b70463 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -958,7 +958,8 @@ def describe(self, by_folder=False, show_hidden=False): folders.extend(folder_name * len(chains)) array_sum.extend([True if c.array_style > -1 else False for c in chains]) - sources.extend(c.source for c in chains) + sources.extend(c.source if not c.edited else 'edited' + for c in chains) for c in chains: for m in bannermap: if m[0] == c._y_keys: banner_ids.append(m[1]) From 9b193b2f5a40fb0761e03ca947c516755a799be8 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 19 Oct 2018 12:03:58 +0200 Subject: [PATCH 540/733] bugfixing letter row on array summary chains --- quantipy/sandbox/sandbox.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 3a9b70463..44a6d0cb8 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -726,10 +726,11 @@ def cut(self, values, ci=None, base=False, tests=False): del c._views[v] else: c._views[v] = names.count(v) - if not tests: - c.sig_test_letters = None - else: - c._frame = c._apply_letter_header(c._frame) + if not c._array_style == 0: + if not tests: + c.sig_test_letters = None + else: + c._frame = c._apply_letter_header(c._frame) c.edited = True return None From ee8f19dc6782e41401e7fe4225f53776ec032163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 19 Oct 2018 13:21:19 +0200 Subject: [PATCH 541/733] filter in batch: checkpoint --- quantipy/core/batch.py | 98 ++++++++++++++-------------------------- quantipy/core/dataset.py | 13 ++++-- tests/test_batch.py | 18 ++++---- 3 files changed, 54 insertions(+), 75 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 168e1bb6b..6b6583f06 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -29,7 +29,7 @@ def edit(*args, **kwargs): if not isinstance(name, list): name = [name] # create DataSet clone to leave global meta data untouched if self.edits_ds is None: - self.edits_ds = self.clone() + self.edits_ds = qp.DataSet.clone(self) ds_clone = self.edits_ds var_edits = [] # args/ kwargs for min_value_count @@ -110,6 +110,7 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.sample_size = None self._verbose_errors = dataset._verbose_errors self._verbose_infos = dataset._verbose_infos + self._dimensions_comp = dataset._dimensions_comp # RENAMED DataSet methods self._dsfilter = qp.DataSet.filter.__func__ @@ -132,8 +133,7 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.x_y_map = None self.x_filter_map = None self.y_on_y = [] - self.y_on_y_filter = {} - self.y_filter_map = None + self.y_filter_map = {} self.forced_names = {} self.summaries = [] self.transposed_arrays = {} @@ -171,7 +171,6 @@ def _update(self): """ self._map_x_to_y() self._map_x_to_filter() - self._map_y_main_filter() self._samplesize_from_batch_filter() for attr in ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', @@ -180,7 +179,7 @@ def _update(self): 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', - 'unwgt_counts', 'y_on_y_filter', 'y_filter_map', 'build_info', + 'unwgt_counts', 'y_filter_map', 'build_info', ]: attr_update = {attr: self.__dict__.get(attr)} self._meta['sets']['batches'][self.name].update(attr_update) @@ -196,12 +195,12 @@ def _load_batch(self): 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', - 'y_on_y_filter', 'y_filter_map', 'build_info', + 'y_filter_map', 'build_info', ]: attr_load = {attr: self._meta['sets']['batches'][self.name].get(attr)} self.__dict__.update(attr_load) - def copy(self, name, b_filter=None, as_addition=False): + def clone(self, name, b_filter=None, as_addition=False): """ Create a copy of Batch instance. @@ -404,7 +403,7 @@ def as_addition(self, batch_name): self.additional = True self.verbatims = [] self.y_on_y = [] - self.y_on_y_filter = {} + self.y_filter_map = {} if self._verbose_infos: msg = ("Batch '{}' specified as addition to Batch '{}'. Any open end " "summaries and 'y_on_y' agg. have been removed!") @@ -666,9 +665,10 @@ def add_filter(self, filter_name, filter_logic=None): None """ name = filter_name.encode('utf-8', errors='ignore') - if self.is_filter(name) and not filter_logic is None: - raise ValueError("'{}' is already a filter-variable. Cannot " - "apply a new logic.") + if self.is_filter(name): + if not filter_logic is None: + raise ValueError("'{}' is already a filter-variable. Cannot " + "apply a new logic.") else: self.add_filter_var(name, filter_logic, False) @@ -740,7 +740,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): else: f_name = '{}_f'.format(title) if self.is_filter(f_name): - logic = intersection([{self.filter: 0, filter_by}]) + logic = intersection([{self.filter: 0}, filter_by]) if not self.take(logic).index.tolist() == self.manifest_filter(f_name): msg = "'{}' is already in use with an other logic." raise ValueError(msg.format(f_name)) @@ -754,11 +754,10 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): else: slicer = self.filter if any(oe['title'] == title for oe in self.verbatims) and not overwrite: - print 'Cannot include %s, as it is already included' % title return None oe = { 'title': title, - 'idx': slicer, + 'filter': slicer, 'columns': oe, 'break_by': break_by, 'incl_nan': incl_nan, @@ -911,10 +910,28 @@ def add_y_on_y(self, name, y_filter=None, main_filter='extend'): if not isinstance(name, basestring): raise TypeError("'name' attribute for add_y_on_y must be a str!") elif not main_filter in ['extend', 'replace'] or main_filter is None: - raise ValueError("'main_filter' mus be either 'extend' or 'replace'.") + raise ValueError("'main_filter' must be either 'extend' or 'replace'.") if not name in self.y_on_y: self.y_on_y.append(name) - self.y_on_y_filter[name] = (main_filter, y_filter) + if y_filter is not None: + logic = {'label': name, 'logic': y_filter} + if main_filter == 'extend': + if self.filter: + f_name = '{}_{}'.format(self.filter, name) + self.extend_filter_var(self.filter, logic, name) + else: + f_name = '{}_f'.format(name) + self.add_filter_var(f_name, logic) + elif main_filter == 'replace': + f_name = '{}_f'.format(name) + self.add_filter_var(f_name, logic) + else: + if main_filter == 'replace': + f_name = None + else: + f_name = self.filter + + self.y_filter_map[name] = f_name self._update() return None @@ -984,19 +1001,7 @@ def _map_x_to_filter(self): for x in self.xks: if self._is_array_item(x): continue - if x in self.extended_filters_per_x: - logic = self.extended_filters_per_x[x] - if not self.filter: - name = 'no_filter_{}'.format(x) - if not name in self.filters: - self.add_filter_var(name, logic) - else: - name = '{}_{}'.format(self.filter, x) - if not name in self.filters: - self.extend_filter_var(self.filter, logic, x) - else: - name = self.filter - + name = self.extended_filters_per_x.get(x, self.filter) mapping[x] = name if self.is_array(x): for x2 in self.sources(x): @@ -1007,41 +1012,6 @@ def _map_x_to_filter(self): self.x_filter_map = mapping return None - def _map_y_main_filter(self): - """ - Get all y_on_y filters and map them with the main filter. - - Returns - ------- - None - """ - mapping = {} - for y_on_y in self.y_on_y: - ext_rep, y_f = self.y_on_y_filter[y_on_y] - if y_f == 'no_filter': - f = None - elif not self.filter: - if not y_f: - f = None - else: - f = 'no_filter_{}'.format(y_on_y) - if not f in self.filters: - self.add_filter_var(f, y_f) - else: - if not y_f: - f = self.filter - elif ext_rep == 'extend': - f = '{}_{}'.format(self.filter, y_on_y) - if not f in self.filters: - self.extend_filter_var(self.filter, y_f, y_on_y) - elif ext_rep == 'replace': - f = y_on_y - if not f in self.filters: - self.add_filter_var(f, y_f) - mapping[y_on_y] = f - self.y_filter_map = mapping - return None - def _check_forced_names(self, variables): """ Store forced names for xks and return adjusted list of downbreaks. diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 640dd5433..5574db35d 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -380,7 +380,7 @@ def _is_delimited_set_mapper(self, mapper): return False def is_filter(self, var): - return True if self.get_property(var, 'recoded_filter') else False + return True if var in self and self.get_property(var, 'recoded_filter') else False def _has_missings(self, var): if self.is_array(var): var = self.sources(var)[0] @@ -3199,7 +3199,11 @@ def add_filter_var(self, name, logic, overwrite=False): values = [(0, 'keep', None)] values += self._transform_filter_logics(logic, 1) self.add_meta(name, 'delimited set', name, [(x, y) for x, y, z in values]) - self.recode(name, {x: z for x, y, z in values[1:]}) + try: + self.recode(name, {x: z for x, y, z in values[1:]}) + except: + print values + self.recode(name, {x: z for x, y, z in values[1:]}) self.recode(name, {0: {name: has_count(len(values)-1)}}, append=True) self._set_property(name, 'recoded_filter', True) return None @@ -3254,7 +3258,10 @@ def _transform_filter_logics(self, logic, start): val = (x, '{} not empty'.format(l), {l: not_count(0)}) elif isinstance(l, dict): if not ('label' in l and 'logic' in l): - raise KeyError("Filter logic must contain 'label' and 'logic'") + l = {'label': str(x), 'logic': l} + if self._verbose_infos: + msg = "Filter logic must contain 'label' and 'logic'" + warnings.warn(msg) val = (x, l['label'], l['logic']) else: raise TypeError('Included logic must be (list of) str or dict.') diff --git a/tests/test_batch.py b/tests/test_batch.py index d0ed4660a..3a390c8c1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -29,10 +29,12 @@ def _get_batch(name, dataset=None, full=False): if not dataset: dataset = _get_dataset() batch = qp.Batch(dataset, name) if full: + if not 'men only' in dataset: + dataset.add_filter_var('men only', {'gender': 1}) batch.add_x(['q1', 'q2', 'q6', 'age']) batch.add_y(['gender', 'q2']) batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') - batch.add_filter('men only', {'gender': 1}) + batch.add_filter('men only') batch.set_weights('weight_a') return batch, dataset @@ -47,7 +49,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 31) + self.assertEqual(len(_get_meta(batch1).keys()), 29) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) @@ -64,7 +66,7 @@ def test_dataset_get_batch(self): 'extended_yks_global', 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'name', 'total', '_filter_slice'] + 'sample_size', 'language', 'name', 'total'] for a in attr: self.assertEqual(batch.__dict__[a], b.__dict__[a]) @@ -132,7 +134,7 @@ def test_add_open_ends(self): batch.add_filter('men only', {'gender': 1}) batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', filter_by={'age': is_ge(49)}) verbatims = _get_meta(batch)['verbatims'][0] - self.assertEqual(len(verbatims['idx']), 118) + self.assertEqual(verbatims['filter'], 'men only_open ends') self.assertEqual(verbatims['columns'], ['q8a', 'q9a']) self.assertEqual(verbatims['break_by'], ['RecordNo']) self.assertEqual(verbatims['title'], 'open ends') @@ -142,7 +144,7 @@ def test_add_open_ends(self): self.assertEqual(len(verbatims), 2) def test_add_filter(self): - batch, ds = _get_batch('test', full=True) + batch, ds = _get_batch('test', full=False) batch.add_x(['q1', 'q2b']) batch.add_y('gender') batch.add_filter('men only', {'gender': 1}) @@ -164,8 +166,8 @@ def test_set_weight(self): def test_copy(self): batch1, ds = _get_batch('test', full=True) - batch2 = batch1.copy('test_copy') - batch3 = batch1.copy('test_copy2', as_addition=True) + batch2 = batch1.clone('test_copy') + batch3 = batch1.clone('test_copy2', as_addition=True) attributes = ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', 'transposed_arrays', 'extended_yks_global', 'extended_yks_per_x', @@ -295,7 +297,7 @@ def test_add_y_on_y(self): batch, ds = _get_batch('test', full=True) b_meta = _get_meta(batch) batch.add_y_on_y('cross', {'age': frange('20-30')}, 'extend') - batch.add_y_on_y('back', 'no_filter', 'replace') + batch.add_y_on_y('back', None, 'replace') self.assertEqual(b_meta['y_filter_map']['back'], None) self.assertEqual(b_meta['y_on_y'], ['cross', 'back']) From a9418fea8d04f13e4c1ea499dd5bdb13ea1c966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 19 Oct 2018 14:40:45 +0200 Subject: [PATCH 542/733] adjust filter recodes in dataset --- quantipy/core/dataset.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 5574db35d..1208ad20f 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3199,11 +3199,7 @@ def add_filter_var(self, name, logic, overwrite=False): values = [(0, 'keep', None)] values += self._transform_filter_logics(logic, 1) self.add_meta(name, 'delimited set', name, [(x, y) for x, y, z in values]) - try: - self.recode(name, {x: z for x, y, z in values[1:]}) - except: - print values - self.recode(name, {x: z for x, y, z in values[1:]}) + self.recode(name, {x: z for x, y, z in values[1:]}) self.recode(name, {0: {name: has_count(len(values)-1)}}, append=True) self._set_property(name, 'recoded_filter', True) return None @@ -3264,7 +3260,12 @@ def _transform_filter_logics(self, logic, start): warnings.warn(msg) val = (x, l['label'], l['logic']) else: - raise TypeError('Included logic must be (list of) str or dict.') + try: + l[0].__name__ in ['_intersection', '_union'] + val = (x, str(x), l) + except: + msg = 'Included logic must be (list of) str or dict/complex logic.' + raise TypeError(msg) values.append(val) return values @@ -3293,6 +3294,8 @@ def manifest_filter(self, name): name: str Name of the filter_variable. """ + if not name: + return self._data.index if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) return self.take({name: 0}) From 47b60c18ca72a5df8fcb17a0972b394fa6d208e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 19 Oct 2018 15:59:31 +0200 Subject: [PATCH 543/733] filter concept in from_batch --- quantipy/core/dataset.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 1208ad20f..e5b973c0b 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -901,7 +901,7 @@ def _from_batch(self, batch_name, include='identity', text_key=[], batch_def = self._meta['sets']['batches'][batch_name] # filter it if needed: f = batch_def['filter'] - if f == 'no_filter': + if f is None: b_ds = self.clone() elif isinstance(f, basestring): b_ds = self.filter(batch_name, {f: 0}) @@ -913,7 +913,7 @@ def _from_batch(self, batch_name, include='identity', text_key=[], if batch_def['additions']: for add_batch in batch_def['additions']: f = add_batch['filter'] - if not f == 'no_filter' and isinstance(f, basestring): + if not f is None and isinstance(f, basestring): if not f in main_variables: main_variables.append(f) if additions in ['full', 'variables']: @@ -1013,7 +1013,7 @@ def _apply_edits_rules(ds, name, b_meta): raise ValueError(msg.format(batch['language'], text_key)) # Create a new instance by filtering or cloning f = batch['filter'] - if f == 'no_filter': + if f is None: b_ds = self.clone() elif isinstance(f, basestring): b_ds = self.filter(batch_name, {f: 0}) @@ -1035,7 +1035,7 @@ def _apply_edits_rules(ds, name, b_meta): for yks in ba['extended_yks_per_x'].values() + ba['exclusive_yks_per_x'].values(): variables += yks if additions in ['full', 'filters']: - variables += batch['filter_names'] + variables += batch['filter_names'] variables = list(set([v for v in variables if not v in ['@', None]])) variables = b_ds.roll_up(variables) b_ds.subset(variables, inplace=True) @@ -6959,7 +6959,6 @@ def populate(self, batches='all', verbose=True): xys = batch['x_y_map'] fs = batch['x_filter_map'] fy = batch['y_filter_map'] - f = batch['filter'] my = batch['yks'] total_len = len(xys) + len(batch['y_on_y']) @@ -6969,13 +6968,13 @@ def populate(self, batches='all', verbose=True): if fs[y[0]] is None: fi = 'no_filter' else: - fi = {fs[y[0]]: {fs[y[0]]: 1}} + fi = {fs[y[0]]: {fs[y[0]]: 0}} stack.add_link(dk, fi, x='@', y=y) else: if fs[x] is None: fi = 'no_filter' else: - fi = {fs[x]: {fs[x]: 1}} + fi = {fs[x]: {fs[x]: 0}} stack.add_link(dk, fi, x=x, y=y) if verbose: done = float(idx) / float(total_len) *100 From 71f5fa921dbf8084ac87011ff45456356f93eb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 11:05:10 +0200 Subject: [PATCH 544/733] filter adjustments in stack.add_tests --- quantipy/core/stack.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 5488d03cc..66231f696 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2718,13 +2718,15 @@ def add_tests(self, _batches='all', verbose=True): 'test_total': sigpro.get('test_total', None), 'groups': 'Tests'}) for yy in batch['y_on_y']: - self.add_link(filters=y_f[yy], x=yks[1:], y=yks, + f = 'no_filter' if y_f[yy] is None else {y_f[yy]: 0} + self.add_link(filters=f, x=yks[1:], y=yks, views=vm_tests, weights=weight) total_len = len(x_y) for idx, xy in enumerate(x_y, 1): x, y = xy if x == '@': continue - self.add_link(filters=x_f[x], x=x, y=y, + f = 'no_filter' if x_f[x] is None else {x_f[x]: 0} + self.add_link(filters=f, x=x, y=y, views=vm_tests, weights=weight) if verbose: done = float(idx) / float(total_len) *100 From bff6c9df7e7c2134bba517c81695225beefa4131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 11:46:07 +0200 Subject: [PATCH 545/733] typo --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e5b973c0b..037907543 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1021,7 +1021,7 @@ def _apply_edits_rules(ds, name, b_meta): b_ds = self.filter(batch_name, f.values()[0]) # Get a subset of variables (xks, yks, oe, weights) - variables = include + variables = include[:] adds = batch['additions'] if additions in ['full', 'variables'] else [] remove = [] for b_name, ba in batches.items(): From b9dcd3d2098fca1f373f5fc8af25da6a39e006d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 12:18:24 +0200 Subject: [PATCH 546/733] tests in stack for new filter --- tests/test_stack.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_stack.py b/tests/test_stack.py index e4d3a7818..1defffe10 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1110,8 +1110,7 @@ def test_stack_aggregate(self): b3, ds = _get_batch('test3', ds, False) b1.add_x(['q1', 'q6', 'age']) b1.add_y(['gender', 'q2']) - b1.add_filter('men only', {'gender': 1}) - b1.extend_filter({'q1':{'age': [20, 21, 22]}}) + b1.extend_filter({'q1': {'age': [20, 21, 22]}}) b1.set_weights('weight_a') b2.add_x(['q1', 'q6']) b2.add_y(['gender', 'q2']) From 87eeeb4a7e4c1948d48e68002abdefad2de12c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 15:43:22 +0200 Subject: [PATCH 547/733] fix dataset.parrot --- quantipy/core/dataset.py | 43 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 037907543..d8ffc28d9 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1021,7 +1021,7 @@ def _apply_edits_rules(ds, name, b_meta): b_ds = self.filter(batch_name, f.values()[0]) # Get a subset of variables (xks, yks, oe, weights) - variables = include[:] + variables = ['@1'] + include[:] adds = batch['additions'] if additions in ['full', 'variables'] else [] remove = [] for b_name, ba in batches.items(): @@ -7003,7 +7003,46 @@ def populate(self, batches='all', verbose=True): def parrot(self): from IPython.display import Image from IPython.display import display + import random + name = [ + '/hd/parrot', '/hd/opensourceparrot', '/hd/middleparrot', + '/hd/rightparrot', '/aussieparrot', '/gothparrot', '/oldtimeyparrot', + '/boredparrot', '/hd/shuffleparrot', '/shufflefurtherparrot', + '/hd/congaparrot', '/reversecongaparrot', '/hd/partyparrot', + '/hd/sadparrot', '/parrotcop', '/hd/fastparrot', '/hd/ultrafastparrot', + '/slowparrot', '/slomoparrot', '/parrotdad', '/hd/dealwithitparrot', + '/fiestaparrot', '/pizzaparrot', '/hamburgerparrot', '/bananaparrot', + '/chillparrot', '/explodyparrot', '/shufflepartyparrot', '/ice-cream-parrot', + '/sassyparrot', '/confusedparrot', '/aussiecongaparrot', + '/aussiereversecongaparrot', '/parrotwave1', '/parrotwave2', + '/parrotwave3', '/parrotwave4', '/parrotwave5', '/parrotwave6', + '/parrotwave7', '/hd/congapartyparrot', '/moonwalkingparrot', + '/thumbsupparrot', '/coffeeparrot', '/hd/parrotmustache', + '/hd/christmasparrot', '/parrotsleep', '/parrotbeer', '/darkbeerparrot', + '/blondesassyparrot', '/bluescluesparrot', '/hd/gentlemanparrot', + '/margaritaparrot', '/dreidelparrot', '/harrypotterparrot', + '/upvotepartyparrot', '/twinsparrot', '/tripletsparrot', + '/stableparrot', '/shipitparrot', '/skiparrot', '/loveparrot', + '/halalparrot', '/hd/wendyparrot', '/hd/popcornparrot', '/hd/donutparrot', + '/evilparrot', '/hd/discoparrot', '/matrixparrot', '/papalparrot', + '/stalkerparrot', '/hd/scienceparrot', '/hd/prideparrot', + '/hd/revolutionparrot', '/fidgetparrot', '/hd/beretparrot', + '/tacoparrot', '/ryangoslingparrot', '/luckyparrot', + '/hd/birthdaypartyparrot', '/hd/jediparrot', '/hd/sithparrot', + '/angryparrot', '/invisibleparrot', '/rotatingparrot', '/cryptoparrot', + '/hd/sushiparrot', '/hd/pumpkinparrot', '/hd/angelparrot', '/hd/bluntparrot', + '/hd/sintparrot', '/hd/pirateparrot', '/hd/ceilingparrot', '/hd/mardigrasparrot', + '/sovjetparrot', '/portalparrot', '/hd/hardhatparrot', '/hd/flyingmoneyparrot', + '/portalorangeparrot', '/portalblueparrot', '/hd/bunnyparrot', + '/hd/norwegianblueparrot', '/hd/transparront', '/fixparrot', + '/brazilianplayerparrot', '/brazilianfanparrot', '/hd/marshmallowparrot', + '/hd/whitewalkerparrot', '/hd/trans-parrot', '/hd/calvinist_parrot'] + url = "https://cultofthepartyparrot.com/parrots/{}.gif" + url = url.format(random.choice(name)) try: - return display(Image(url="https://m.popkey.co/3a9f4b/jZZ83.gif")) + return display(Image( + url=url)) except: print ':sad_parrot: Looks like the parrot url is not longer there!' + + From 31382472e0f31d5bc08fe9321f7f018cf5b32dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 15:45:12 +0200 Subject: [PATCH 548/733] typo --- quantipy/core/dataset.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index d8ffc28d9..50f6abc43 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -7038,10 +7038,8 @@ def parrot(self): '/brazilianplayerparrot', '/brazilianfanparrot', '/hd/marshmallowparrot', '/hd/whitewalkerparrot', '/hd/trans-parrot', '/hd/calvinist_parrot'] url = "https://cultofthepartyparrot.com/parrots/{}.gif" - url = url.format(random.choice(name)) try: - return display(Image( - url=url)) + return display(Image(url=url.format(random.choice(name)))) except: print ':sad_parrot: Looks like the parrot url is not longer there!' From 6e1d5d29eddbe0dfd4f4861f8351c2fb34355e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 16:46:42 +0200 Subject: [PATCH 549/733] typo --- quantipy/core/dataset.py | 71 +++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 50f6abc43..9cf04662c 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -7000,44 +7000,47 @@ def populate(self, batches='all', verbose=True): # ============================================================================ - def parrot(self): + @staticmethod + def parrot(): from IPython.display import Image from IPython.display import display import random name = [ - '/hd/parrot', '/hd/opensourceparrot', '/hd/middleparrot', - '/hd/rightparrot', '/aussieparrot', '/gothparrot', '/oldtimeyparrot', - '/boredparrot', '/hd/shuffleparrot', '/shufflefurtherparrot', - '/hd/congaparrot', '/reversecongaparrot', '/hd/partyparrot', - '/hd/sadparrot', '/parrotcop', '/hd/fastparrot', '/hd/ultrafastparrot', - '/slowparrot', '/slomoparrot', '/parrotdad', '/hd/dealwithitparrot', - '/fiestaparrot', '/pizzaparrot', '/hamburgerparrot', '/bananaparrot', - '/chillparrot', '/explodyparrot', '/shufflepartyparrot', '/ice-cream-parrot', - '/sassyparrot', '/confusedparrot', '/aussiecongaparrot', - '/aussiereversecongaparrot', '/parrotwave1', '/parrotwave2', - '/parrotwave3', '/parrotwave4', '/parrotwave5', '/parrotwave6', - '/parrotwave7', '/hd/congapartyparrot', '/moonwalkingparrot', - '/thumbsupparrot', '/coffeeparrot', '/hd/parrotmustache', - '/hd/christmasparrot', '/parrotsleep', '/parrotbeer', '/darkbeerparrot', - '/blondesassyparrot', '/bluescluesparrot', '/hd/gentlemanparrot', - '/margaritaparrot', '/dreidelparrot', '/harrypotterparrot', - '/upvotepartyparrot', '/twinsparrot', '/tripletsparrot', - '/stableparrot', '/shipitparrot', '/skiparrot', '/loveparrot', - '/halalparrot', '/hd/wendyparrot', '/hd/popcornparrot', '/hd/donutparrot', - '/evilparrot', '/hd/discoparrot', '/matrixparrot', '/papalparrot', - '/stalkerparrot', '/hd/scienceparrot', '/hd/prideparrot', - '/hd/revolutionparrot', '/fidgetparrot', '/hd/beretparrot', - '/tacoparrot', '/ryangoslingparrot', '/luckyparrot', - '/hd/birthdaypartyparrot', '/hd/jediparrot', '/hd/sithparrot', - '/angryparrot', '/invisibleparrot', '/rotatingparrot', '/cryptoparrot', - '/hd/sushiparrot', '/hd/pumpkinparrot', '/hd/angelparrot', '/hd/bluntparrot', - '/hd/sintparrot', '/hd/pirateparrot', '/hd/ceilingparrot', '/hd/mardigrasparrot', - '/sovjetparrot', '/portalparrot', '/hd/hardhatparrot', '/hd/flyingmoneyparrot', - '/portalorangeparrot', '/portalblueparrot', '/hd/bunnyparrot', - '/hd/norwegianblueparrot', '/hd/transparront', '/fixparrot', - '/brazilianplayerparrot', '/brazilianfanparrot', '/hd/marshmallowparrot', - '/hd/whitewalkerparrot', '/hd/trans-parrot', '/hd/calvinist_parrot'] - url = "https://cultofthepartyparrot.com/parrots/{}.gif" + '/angryparrot', '/aussiecongaparrot', '/aussieparrot', + '/aussiereversecongaparrot', '/bananaparrot', '/blondesassyparrot', + '/bluescluesparrot', '/boredparrot', '/brazilianfanparrot', + '/brazilianplayerparrot', '/chillparrot', '/coffeeparrot', + '/confusedparrot', '/cryptoparrot', '/darkbeerparrot', + '/dreidelparrot', '/evilparrot', '/explodyparrot', '/fidgetparrot', + '/fiestaparrot', '/fixparrot', '/gothparrot', '/halalparrot', + '/hamburgerparrot', '/harrypotterparrot', '/ice-cream-parrot', + '/invisibleparrot', '/loveparrot', '/luckyparrot', '/margaritaparrot', + '/matrixparrot', '/moonwalkingparrot', '/oldtimeyparrot', '/papalparrot', + '/parrotbeer', '/parrotcop', '/parrotdad', '/parrotsleep', + '/parrotwave1', '/parrotwave2', '/parrotwave3', '/parrotwave4', + '/parrotwave5', '/parrotwave6', '/parrotwave7', '/pizzaparrot', + '/portalblueparrot', '/portalorangeparrot', '/portalparrot', + '/reversecongaparrot', '/rotatingparrot', '/ryangoslingparrot', + '/sassyparrot', '/shipitparrot', '/shufflefurtherparrot', + '/shufflepartyparrot', '/skiparrot', '/slomoparrot', '/slowparrot', + '/sovjetparrot', '/stableparrot', '/stalkerparrot', '/tacoparrot', + '/thumbsupparrot', '/tripletsparrot', '/twinsparrot', '/upvotepartyparrot'] + hd_names = [ + '/angelparrot', '/beretparrot', '/birthdaypartyparrot', + '/bluntparrot', '/bunnyparrot', '/calvinist_parrot', + '/ceilingparrot', '/christmasparrot', '/congaparrot', + '/congapartyparrot', '/dealwithitparrot', '/discoparrot', + '/donutparrot', '/fastparrot', '/flyingmoneyparrot', + '/gentlemanparrot', '/hardhatparrot', '/jediparrot', + '/mardigrasparrot', '/marshmallowparrot', '/middleparrot', + '/norwegianblueparrot', '/opensourceparrot', '/parrot', + '/parrotmustache', '/partyparrot', '/pirateparrot', '/popcornparrot', + '/prideparrot', '/pumpkinparrot', '/revolutionparrot', '/rightparrot', + '/sadparrot', '/scienceparrot', '/shuffleparrot', '/sintparrot', + '/sithparrot', '/sushiparrot', '/trans-parrot', '/transparront', + '/ultrafastparrot', '/wendyparrot', '/whitewalkerparrot'] + name += ['/hd%s' % n for n in hd_names] + url = "https://cultofthepartyparrot.com/parrots{}.gif" try: return display(Image(url=url.format(random.choice(name)))) except: From a7d2c90a6a1bd78560932b30e5031be11fc85c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 22 Oct 2018 17:06:13 +0200 Subject: [PATCH 550/733] adjust raise message --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 6b6583f06..beb9c8137 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -668,7 +668,7 @@ def add_filter(self, filter_name, filter_logic=None): if self.is_filter(name): if not filter_logic is None: raise ValueError("'{}' is already a filter-variable. Cannot " - "apply a new logic.") + "apply a new logic.".format(name)) else: self.add_filter_var(name, filter_logic, False) From 5687cc77a54aa5cf388d9cc2df88bed5eb7b8741 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 23 Oct 2018 11:14:06 +0200 Subject: [PATCH 551/733] add_variables(), _update and load methods updated --- quantipy/core/batch.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index a2dad9ae2..fd81f0026 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -122,6 +122,7 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): sets['batches'][name] = {'name': name, 'additions': []} self.xks = [] self.yks = ['@'] + self.variables = [] self.total = True self.extended_yks_global = None self.extended_yks_per_x = {} @@ -174,7 +175,7 @@ def _update(self): self._map_x_to_filter() self._map_y_main_filter() self._samplesize_from_batch_filter() - for attr in ['xks', 'yks', 'filter', 'filter_names', + for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', @@ -190,7 +191,7 @@ def _load_batch(self): """ Fill batch attributes with information from meta. """ - for attr in ['xks', 'yks', 'filter', 'filter_names', + for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', @@ -413,6 +414,28 @@ def as_addition(self, batch_name): self._update() return None + @modify(to_list='varlist') + def add_variables(self, varlist): + """ + Text + + Parameters + ---------- + varlist : list + A list of variable names. + + Returns + ------- + None + """ + if '@' in varlist: varlist.remove('@') + for v in varlist: + if not v in self.variables: + self.variables.append(v) + self._update() + return None + + @modify(to_list='xks') def add_x(self, xks): """ From 43217bb6a762478b26a1b2bf5995d2ff6cf96faa Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 23 Oct 2018 11:32:08 +0200 Subject: [PATCH 552/733] add_x -> add_downbreak, add_y -> add_crossbreak, depr warnings for old methods. --- quantipy/core/batch.py | 63 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index fd81f0026..538e77152 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -436,14 +436,14 @@ def add_variables(self, varlist): return None - @modify(to_list='xks') - def add_x(self, xks): + @modify(to_list='dbrk') + def add_downbreak(self, dbrk): """ - Set the x (downbreak) variables of the Batch. + Set the downbreak (x) variables of the Batch. Parameters ---------- - xks: str, list of str, dict, list of dict + dbrk: str, list of str, dict, list of dict Names of variables that are used as downbreaks. Forced names for Excel outputs can be given in a dict, for example: xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] @@ -452,13 +452,34 @@ def add_x(self, xks): ------- None """ - clean_xks = self._check_forced_names(xks) + clean_xks = self._check_forced_names(dbrk) self.xks = self.unroll(clean_xks, both='all') self._update() masks = [x for x in self.xks if x in self.masks()] self.make_summaries(masks, [], _verbose=False) return None + + @modify(to_list='xks') + def add_x(self, xks): + """ + Set the x (downbreak) variables of the Batch. + + Parameters + ---------- + xks: str, list of str, dict, list of dict + Names of variables that are used as downbreaks. Forced names for + Excel outputs can be given in a dict, for example: + xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] + + Returns + ------- + None + """ + w = "'add_x()' will be deprecated in a future version. Please use 'add_downbreak()' instead!" + warnings.warn(w) + self.add_downbreak(xks) + @modify(to_list=['ext_xks']) def extend_x(self, ext_xks): """ @@ -631,18 +652,18 @@ def add_total(self, total=True): if self._verbose_infos: print 'sigtests are removed from batch.' self.total = total - self.add_y(self.yks) + self.add_crossbreak(self.yks) return None - @modify(to_list='yks') - @verify(variables={'yks': 'both'}, categorical='yks') - def add_y(self, yks): + @modify(to_list='xbrk') + @verify(variables={'xbrk': 'both'}, categorical='xbrk') + def add_crossbreak(self, xbrk): """ Set the y (crossbreak/banner) variables of the Batch. Parameters ---------- - yks: str, list of str + xbrk: str, list of str Variables that are added as crossbreaks. '@'/ total is added automatically. @@ -650,7 +671,7 @@ def add_y(self, yks): ------- None """ - yks = [y for y in yks if not y=='@'] + yks = [y for y in xbrk if not y=='@'] yks = self.unroll(yks) if self.total: yks = ['@'] + yks @@ -658,6 +679,26 @@ def add_y(self, yks): self._update() return None + @modify(to_list='yks') + @verify(variables={'yks': 'both'}, categorical='yks') + def add_y(self, yks): + """ + Set the y (crossbreak/banner) variables of the Batch. + + Parameters + ---------- + yks: str, list of str + Variables that are added as crossbreaks. '@'/ total is added + automatically. + + Returns + ------- + None + """ + w = "'add_y()' will be deprecated in a future version. Please use 'add_crossbreak()' instead!" + warnings.warn(w) + self.add_crossbreak(yks) + def add_x_per_y(self, x_on_y_map): """ Add individual combinations of x and y variables to the Batch. From 418293f6b73126d0b72f47617a2f1b3055da1fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Oct 2018 15:40:51 +0200 Subject: [PATCH 553/733] fix in hmerge: updating of existing cols was broken --- quantipy/core/dataset.py | 2 +- quantipy/core/tools/dp/prep.py | 39 ++++++++++++++++------------------ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 2178abf3c..777db8c00 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -189,7 +189,7 @@ def strings(self): def created(self): return [v for v in self.variables() if self.get_property(v, 'created')] - + def batches(self): if 'batches' in self._meta['sets']: return self._meta['sets']['batches'].keys() diff --git a/quantipy/core/tools/dp/prep.py b/quantipy/core/tools/dp/prep.py index 00fa32da0..374aa9392 100644 --- a/quantipy/core/tools/dp/prep.py +++ b/quantipy/core/tools/dp/prep.py @@ -1466,7 +1466,7 @@ def hmerge(dataset_left, dataset_right, on=None, left_on=None, right_on=None, """ def _merge_delimited_sets(x): codes = [] - x = x.replace('nan', '') + x = str(x).replace('nan', '') for c in x.split(';'): if not c: continue @@ -1494,10 +1494,11 @@ def _merge_delimited_sets(x): data_left = dataset_left[1].copy() if isinstance(dataset_right, tuple): dataset_right = [dataset_right] - for ds_right in dataset_right: meta_right = copy.deepcopy(ds_right[0]) data_right = ds_right[1].copy() + slicer = data_right[right_on].isin(data_left[left_on].values.tolist()) + data_right = data_right.loc[slicer, :] if verbose: print '\n', 'Checking metadata...' @@ -1521,44 +1522,40 @@ def _merge_delimited_sets(x): if verbose: print '\n', 'Merging data...' + # update columns which are in left and in right data if col_updates: - updata_left = data_left.set_index([left_on])[col_updates].copy() + updata_left = data_left.copy() + updata_left['org_idx'] = updata_left.index.tolist() + updata_left = updata_left.set_index([left_on])[col_updates+['org_idx']] + updata_right = data_right.set_index( + right_on, drop=not update_right_on)[col_updates].copy() sets = [c for c in col_updates if meta_left['columns'][c]['type'] == 'delimited set'] non_sets = [c for c in col_updates if not c in sets] - if update_right_on: - updata_right = data_right.set_index(right_on, drop=False)[col_updates].copy() - else: - updata_right = data_right.set_index(right_on)[col_updates].copy() if verbose: print '------ updating data for known columns' updata_left.update(updata_right[non_sets]) - for update_col in non_sets: - if verbose: - print "..{}".format(update_col) - try: - data_left[update_col] = updata_left[update_col].astype( - data_left[update_col].dtype).values - except: - data_left[update_col] = updata_left[update_col].astype( - 'object').values if merge_existing: for col in sets: if not (merge_existing == 'all' or col in merge_existing): continue if verbose: - print "..{}".format(update_col) - merge_set = data_left[col].astype(str) + data_right[col].astype(str) - data_left[col] = merge_set.apply(lambda x: _merge_delimited_sets(x)) - + print "..{}".format(col) + updata_left[col] = updata_left[col].combine( + updata_right[col], + lambda x, y: _merge_delimited_sets(str(x)+str(y))) + updata_left.reset_index(inplace=True) + for col in col_updates: + data_left[col] = updata_left[col] + + # append completely new columns if verbose: print '------ appending new columns' new_cols = [col for col in cols if not col in col_updates] if update_right_on: new_cols.append(right_on) - # This will be passed into pd.DataFrame.merge() kwargs = {'left_on': left_on, 'right_on': right_on, 'how': 'left'} From ab6255cb1fa1ff0d9a2f83212670a7cfa6570730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Oct 2018 16:26:09 +0200 Subject: [PATCH 554/733] keep type in merge --- quantipy/core/tools/dp/prep.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quantipy/core/tools/dp/prep.py b/quantipy/core/tools/dp/prep.py index 374aa9392..84b479248 100644 --- a/quantipy/core/tools/dp/prep.py +++ b/quantipy/core/tools/dp/prep.py @@ -1547,7 +1547,7 @@ def _merge_delimited_sets(x): lambda x, y: _merge_delimited_sets(str(x)+str(y))) updata_left.reset_index(inplace=True) for col in col_updates: - data_left[col] = updata_left[col] + data_left[col] = updata_left[col].astype(data_left[col].dtype) # append completely new columns if verbose: @@ -1576,8 +1576,6 @@ def _merge_delimited_sets(x): return meta_left, data_left - - def vmerge(dataset_left=None, dataset_right=None, datasets=None, on=None, left_on=None, right_on=None, row_id_name=None, left_id=None, right_id=None, row_ids=None, From 57bacfea0bd2d4750a3a82b95b11deab6faaeeda Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 23 Oct 2018 16:47:39 +0200 Subject: [PATCH 555/733] checkpoint: Batch.variables() --- quantipy/core/batch.py | 1 + quantipy/core/dataset.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 538e77152..2a1ee2e07 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -429,6 +429,7 @@ def add_variables(self, varlist): None """ if '@' in varlist: varlist.remove('@') + if '@1' in varlist: varlist.remove('@1') for v in varlist: if not v in self.variables: self.variables.append(v) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 2178abf3c..505e515a2 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1047,7 +1047,7 @@ def _manifest_filters(ds, batch_name): for b_name, ba in batches.items(): if not b_name in [batch_name] + adds: continue - variables += ba['xks'] + ba['yks'] + variables += ba['xks'] + ba['yks'] + ba['variables'] for oe in ba['verbatims']: variables += oe['columns'] variables += ba['weights'] From 79a61a1dd1d3d6544fe907a62e906ee686f0e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Oct 2018 16:59:10 +0200 Subject: [PATCH 556/733] checkpoint --- quantipy/core/stack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 66231f696..dd319d857 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -1856,7 +1856,7 @@ def _x_y_f_w_map(self, dk, batches='all'): """ def _append_loop(mapping, x, fi, w, ys): fn = 'no_filter' if fi is None else fi - f = 'no_filter' if fi is None else {fi: {fi: 1}} + f = 'no_filter' if fi is None else {fi: {fi: 0}} if not x in mapping: mapping[x] = {fn: {'f': f, tuple(w): ys}} elif not fn in mapping[x]: @@ -2718,14 +2718,14 @@ def add_tests(self, _batches='all', verbose=True): 'test_total': sigpro.get('test_total', None), 'groups': 'Tests'}) for yy in batch['y_on_y']: - f = 'no_filter' if y_f[yy] is None else {y_f[yy]: 0} + f = ['no_filter'] if y_f[yy] is None else (y_f[yy], {y_f[yy]: 0}) self.add_link(filters=f, x=yks[1:], y=yks, views=vm_tests, weights=weight) total_len = len(x_y) for idx, xy in enumerate(x_y, 1): x, y = xy if x == '@': continue - f = 'no_filter' if x_f[x] is None else {x_f[x]: 0} + f = ['no_filter'] if x_f[x] is None else (x_f[x], {x_f[x]: 0}) self.add_link(filters=f, x=x, y=y, views=vm_tests, weights=weight) if verbose: From c945127f2707b892845b048db8e6edb9115ed6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Oct 2018 17:49:56 +0200 Subject: [PATCH 557/733] checkpoint --- quantipy/core/stack.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index dd319d857..418e38387 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -779,6 +779,7 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, else: dataset = qp.DataSet('stack') dataset.from_components(self[dk].data, self[dk].meta) + print logic f_dataset = dataset.filter(filter_def, logic, inplace=False) self[dk][filter_def].data = f_dataset._data self[dk][filter_def].meta = f_dataset._meta @@ -2718,14 +2719,14 @@ def add_tests(self, _batches='all', verbose=True): 'test_total': sigpro.get('test_total', None), 'groups': 'Tests'}) for yy in batch['y_on_y']: - f = ['no_filter'] if y_f[yy] is None else (y_f[yy], {y_f[yy]: 0}) + f = ['no_filter'] if y_f[yy] is None else {y_f[yy]: 0} self.add_link(filters=f, x=yks[1:], y=yks, views=vm_tests, weights=weight) total_len = len(x_y) for idx, xy in enumerate(x_y, 1): x, y = xy if x == '@': continue - f = ['no_filter'] if x_f[x] is None else (x_f[x], {x_f[x]: 0}) + f = ['no_filter'] if x_f[x] is None else {x_f[x]: 0} self.add_link(filters=f, x=x, y=y, views=vm_tests, weights=weight) if verbose: From 98610b25943db1ba49322ee5870b38c83355806d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 24 Oct 2018 10:45:57 +0200 Subject: [PATCH 558/733] =?UTF-8?q?allow=20overwriting=20of=20batch=20filt?= =?UTF-8?q?ers=C3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- quantipy/core/batch.py | 10 ++++++---- quantipy/core/stack.py | 5 ++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index beb9c8137..647cc5433 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -649,7 +649,7 @@ def add_x_per_y(self, x_on_y_map): if isinstance(x, tuple): x = {x[0]: x[1]} return None - def add_filter(self, filter_name, filter_logic=None): + def add_filter(self, filter_name, filter_logic=None, overwrite=False): """ Apply a (global) filter to all the variables found in the Batch. @@ -666,12 +666,14 @@ def add_filter(self, filter_name, filter_logic=None): """ name = filter_name.encode('utf-8', errors='ignore') if self.is_filter(name): - if not filter_logic is None: + if not (filter_logic is None or overwrite): raise ValueError("'{}' is already a filter-variable. Cannot " "apply a new logic.".format(name)) - else: - self.add_filter_var(name, filter_logic, False) + else: + self.drop(name) + print 'Overwrite filter var: {}'.format(name) + self.add_filter_var(name, filter_logic, False) self.filter = name if not name in self.filter_names: self.filter_names.append(name) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 418e38387..64f7e3f5f 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -779,7 +779,6 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, else: dataset = qp.DataSet('stack') dataset.from_components(self[dk].data, self[dk].meta) - print logic f_dataset = dataset.filter(filter_def, logic, inplace=False) self[dk][filter_def].data = f_dataset._data self[dk][filter_def].meta = f_dataset._meta @@ -2719,14 +2718,14 @@ def add_tests(self, _batches='all', verbose=True): 'test_total': sigpro.get('test_total', None), 'groups': 'Tests'}) for yy in batch['y_on_y']: - f = ['no_filter'] if y_f[yy] is None else {y_f[yy]: 0} + f = ['no_filter'] if y_f[yy] is None else {y_f[yy]: {y_f[yy]: 0}} self.add_link(filters=f, x=yks[1:], y=yks, views=vm_tests, weights=weight) total_len = len(x_y) for idx, xy in enumerate(x_y, 1): x, y = xy if x == '@': continue - f = ['no_filter'] if x_f[x] is None else {x_f[x]: 0} + f = ['no_filter'] if x_f[x] is None else {x_f[x]: {x_f[x]: 0}} self.add_link(filters=f, x=x, y=y, views=vm_tests, weights=weight) if verbose: From 76a4328248cad19a87648c7ad7d86376ce19d79a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 24 Oct 2018 11:24:11 +0200 Subject: [PATCH 559/733] emptying .variables before adding new list of variables --- quantipy/core/batch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 2a1ee2e07..3fd6b37d7 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -414,6 +414,7 @@ def as_addition(self, batch_name): self._update() return None + @modify(to_list='varlist') def add_variables(self, varlist): """ @@ -428,6 +429,7 @@ def add_variables(self, varlist): ------- None """ + self.variables = [] if '@' in varlist: varlist.remove('@') if '@1' in varlist: varlist.remove('@1') for v in varlist: From d59d026af36b69e959cdeabedde025ad84dbfdde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 24 Oct 2018 12:16:58 +0200 Subject: [PATCH 560/733] checkpoint --- quantipy/core/batch.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 647cc5433..b64fcf130 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -669,11 +669,13 @@ def add_filter(self, filter_name, filter_logic=None, overwrite=False): if not (filter_logic is None or overwrite): raise ValueError("'{}' is already a filter-variable. Cannot " "apply a new logic.".format(name)) - else: + elif overwrite: self.drop(name) print 'Overwrite filter var: {}'.format(name) + self.add_filter_var(name, filter_logic, overwrite) - self.add_filter_var(name, filter_logic, False) + else: + self.add_filter_var(name, filter_logic, overwrite) self.filter = name if not name in self.filter_names: self.filter_names.append(name) From 81a97b37aa970b1af44cca9585b5b825766505a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 24 Oct 2018 16:31:35 +0200 Subject: [PATCH 561/733] fix ignore renames in batch props for copy --- quantipy/core/dataset.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9cf04662c..89d059c17 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1035,7 +1035,9 @@ def _apply_edits_rules(ds, name, b_meta): for yks in ba['extended_yks_per_x'].values() + ba['exclusive_yks_per_x'].values(): variables += yks if additions in ['full', 'filters']: - variables += batch['filter_names'] + print b_name + print ba['filter_names'] + variables += ba['filter_names'] variables = list(set([v for v in variables if not v in ['@', None]])) variables = b_ds.roll_up(variables) b_ds.subset(variables, inplace=True) @@ -3205,7 +3207,7 @@ def add_filter_var(self, name, logic, overwrite=False): return None @modify(to_list=['logic']) - def extend_filter_var(self, name, logic, extend_as=None): + def extend_filter_var(self, name, logic, extend_as=None, check=None): """ Extend logic of an existing filter-variable. @@ -3979,7 +3981,7 @@ def copy(self, name, suffix='rec', copy_data=True, slicer=None, copy_only=None, self._data[copy_name] = np.NaN # run the renaming for the copied variable - self.rename_from_mapper(renames, keep_original=True) + self.rename_from_mapper(renames, keep_original=True, ignore_batch_props=True) # set type 'created' if is_array: for s in self.sources(copy_name): @@ -4908,7 +4910,8 @@ def rename(self, name, new_name): return None - def rename_from_mapper(self, mapper, keep_original=False): + def rename_from_mapper(self, mapper, keep_original=False, + ignore_batch_props=False): """ Rename meta objects and data columns using mapper. @@ -4940,7 +4943,7 @@ def rename_properties(mapper): if rs: self._set_property(rec, 'recoded_stat', mapper[rs]) return None - def rename_meta(meta, mapper): + def rename_meta(meta, mapper, ignore_batch_props): """ Rename lib@values, masks, set items and columns using mapper. """ @@ -4949,7 +4952,7 @@ def rename_meta(meta, mapper): rename_masks(meta['masks'], mapper, keep_original) rename_columns(meta['columns'], mapper, keep_original) rename_sets(meta['sets'], mapper, keep_original) - if 'batches' in meta['sets']: + if 'batches' in meta['sets'] and not ignore_batch_props: rename_batch_properties(meta['sets']['batches'], mapper) if not keep_original: rename_set_items(meta['sets'], mapper) @@ -5064,7 +5067,7 @@ def rename_set_items(sets, mapper): except (AttributeError, KeyError, TypeError, ValueError): pass - rename_meta(self._meta, mapper) + rename_meta(self._meta, mapper, ignore_batch_props) if not keep_original: self._data.rename(columns=mapper, inplace=True) def dimensionizing_mapper(self, names=None): From a01210f119195b846ffb49e7600ed358d295a9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 24 Oct 2018 16:34:33 +0200 Subject: [PATCH 562/733] remove prints --- quantipy/core/dataset.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 89d059c17..21c320bb1 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1035,8 +1035,6 @@ def _apply_edits_rules(ds, name, b_meta): for yks in ba['extended_yks_per_x'].values() + ba['exclusive_yks_per_x'].values(): variables += yks if additions in ['full', 'filters']: - print b_name - print ba['filter_names'] variables += ba['filter_names'] variables = list(set([v for v in variables if not v in ['@', None]])) variables = b_ds.roll_up(variables) From 9bd7761235b1eef47752ec8deacc49f2e7fede22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 25 Oct 2018 11:13:07 +0200 Subject: [PATCH 563/733] checkpoint --- quantipy/core/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 21c320bb1..015094867 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3240,6 +3240,7 @@ def extend_filter_var(self, name, logic, extend_as=None, check=None): self.uncode(f_name, {0: {f_name: 0}}) values = self._transform_filter_logics(logic, max(self.codes(f_name))+1) self.extend_values(f_name, values) + self.set_variable_text(f_name, '{} _ {}'.format(self.text(f_name), extend_as)) self.recode(f_name, {x: z for x, y, z in values}, append=True) self.recode(f_name, {0: {f_name: has_count(len(self.codes(f_name))-1)}}, append=True) return None From 2f19e588608831b3601378afe18a7df7ebd8f759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 25 Oct 2018 11:48:51 +0200 Subject: [PATCH 564/733] allow inserting cm by neg values --- quantipy/sandbox/sandbox.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 05608b313..39091d71c 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -488,7 +488,7 @@ def insert(self, other_cm, index=-1, safe_names=False): """ if not isinstance(other_cm, ChainManager): raise ValueError("other_cm must be a quantipy.ChainManager instance.") - if index > -1: + if not index == -1: before_c = self.__chains[:index+1] after_c = self.__chains[index+1:] new_chains = before_c + other_cm.__chains + after_c @@ -732,7 +732,7 @@ def cut(self, values, ci=None, base=False, tests=False): else: c._frame = c._apply_letter_header(c._frame) c.edited = True - + return None def join(self, title='Summary'): @@ -743,7 +743,7 @@ def join(self, title='Summary'): ---------- title : {str, 'auto'}, default 'Summary' The new title for the joined axis' index representation. - + Returns ------- None @@ -756,7 +756,7 @@ def join(self, title='Summary'): new_labels = [] for c in chains: new_label = [] - if c.sig_test_letters: + if c.sig_test_letters: c._remove_letter_header() c._frame = c._apply_letter_header(c._frame) df = c.dataframe @@ -771,15 +771,15 @@ def join(self, title='Summary'): df.index = join_idx df.rename(columns={c._x_keys[0]: 'Total'}, inplace=True) - + if not c.array_style == 0: custom_views.extend(c._views_per_rows()) else: df.columns.set_levels(levels=[title]*totalmul, level=0, inplace=True) concat_dfs.append(df) - - new_df = pd.concat(concat_dfs, axis=0, join='inner') + + new_df = pd.concat(concat_dfs, axis=0, join='inner') self.chains[0]._frame = new_df self.reorder([0]) self.rename({self.single_names[0]: title}) @@ -2168,7 +2168,7 @@ def _counts_first(self): else: return False - #@property + #@property def _views_per_rows(self): """ """ @@ -2384,7 +2384,7 @@ def _view_idxs(self, view_tags, keep_tests=True, keep_bases=True, names=False, c if row[0] in view_tags: order.append(view_tags.index(row[0])) idxs.append(i) - if nested: + if nested: names.append(self._views_per_rows()[rp_idx][i]) else: names.append(self._views_per_rows()[i]) From 17fd9b353914d2c817cf06906744e1af0eef12a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 25 Oct 2018 15:19:08 +0200 Subject: [PATCH 565/733] dataset compare_filter --- quantipy/core/batch.py | 8 +++++++- quantipy/core/dataset.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index b64fcf130..4287764dc 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -36,9 +36,15 @@ def edit(*args, **kwargs): if dataset_func.func_name == 'min_value_count': if len(args) < 3 and not 'weight' in kwargs: kwargs['weight'] = self.weights[0] + if len(args) < 4 and not 'condition' in kwargs: if not self.filter == 'no_filter': kwargs['condition'] = self.filter.values()[0] + # args/ kwargs for sorting + elif dataset_func.func_name == 'sorting': + if len(args) < 7 and not 'sort_by_weight' in kwargs: + kwargs['sort_by_weight'] = self.weights[0] + for n in name: is_array = self.is_array(n) @@ -77,7 +83,7 @@ def edit(*args, **kwargs): if ds_clone._has_categorical_data(n): self.meta_edits['lib'][n] = ds_clone._meta['lib']['values'][n] self.meta_edits[n] = meta - if dataset_func.func_name in ['hiding', 'slicing', 'min_value_count']: + if dataset_func.func_name in ['hiding', 'slicing', 'min_value_count', 'sorting']: self._update() return edit diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 015094867..538c1b492 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3301,6 +3301,27 @@ def manifest_filter(self, name): raise KeyError('{} is no valid filter-variable.'.format(name)) return self.take({name: 0}) + @modify(to_list=['name2']) + @verify(variables={'name1': 'both', 'name2': 'both'}) + def compare_filter(self, name1, name2): + """ + Show if filters result in the same index. + + Parameters + ---------- + name1: str + Name of the first filter variable + name2: str/ list of st + Name(s) of the filter variable(s) to compare with. + """ + if not all(self.is_filter(f) for f in [name1] + name2): + raise ValueError('Can only compare filter variables') + equal = True + for f in name2: + if not all(self.manifest_filter(name1) == self.manifest_filter(f)): + equal = False + return equal + # ------------------------------------------------------------------------ # extending / merging # ------------------------------------------------------------------------ From d2d3c7b5393706312bb59d0c51fad4b790be006c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 25 Oct 2018 16:28:16 +0200 Subject: [PATCH 566/733] qp parrot! --- quantipy/__init__.py | 2 ++ quantipy/core/dataset.py | 50 ----------------------------- quantipy/core/helpers/functions.py | 10 ++++++ quantipy/core/helpers/parrot.gif | Bin 0 -> 10389 bytes 4 files changed, 12 insertions(+), 50 deletions(-) create mode 100644 quantipy/core/helpers/parrot.gif diff --git a/quantipy/__init__.py b/quantipy/__init__.py index fa5b33000..50480928f 100644 --- a/quantipy/__init__.py +++ b/quantipy/__init__.py @@ -12,6 +12,7 @@ from quantipy.core.view_generators.view_mapper import ViewMapper from quantipy.core.view_generators.view_maps import QuantipyViews from quantipy.core.view_generators.view_specs import (net, calc, ViewManager) +from quantipy.core.helpers.functions import parrot import quantipy.core.helpers.functions as helpers import quantipy.core.tools.dp as dp import quantipy.core.tools.view as v @@ -32,3 +33,4 @@ from quantipy.core.builds.powerpoint.pptx_painter import PowerPointPainter from quantipy.version import version as __version__ + diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 538c1b492..c7da079c1 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -7020,53 +7020,3 @@ def populate(self, batches='all', verbose=True): if verbose: print '\n' return stack - -# ============================================================================ - - @staticmethod - def parrot(): - from IPython.display import Image - from IPython.display import display - import random - name = [ - '/angryparrot', '/aussiecongaparrot', '/aussieparrot', - '/aussiereversecongaparrot', '/bananaparrot', '/blondesassyparrot', - '/bluescluesparrot', '/boredparrot', '/brazilianfanparrot', - '/brazilianplayerparrot', '/chillparrot', '/coffeeparrot', - '/confusedparrot', '/cryptoparrot', '/darkbeerparrot', - '/dreidelparrot', '/evilparrot', '/explodyparrot', '/fidgetparrot', - '/fiestaparrot', '/fixparrot', '/gothparrot', '/halalparrot', - '/hamburgerparrot', '/harrypotterparrot', '/ice-cream-parrot', - '/invisibleparrot', '/loveparrot', '/luckyparrot', '/margaritaparrot', - '/matrixparrot', '/moonwalkingparrot', '/oldtimeyparrot', '/papalparrot', - '/parrotbeer', '/parrotcop', '/parrotdad', '/parrotsleep', - '/parrotwave1', '/parrotwave2', '/parrotwave3', '/parrotwave4', - '/parrotwave5', '/parrotwave6', '/parrotwave7', '/pizzaparrot', - '/portalblueparrot', '/portalorangeparrot', '/portalparrot', - '/reversecongaparrot', '/rotatingparrot', '/ryangoslingparrot', - '/sassyparrot', '/shipitparrot', '/shufflefurtherparrot', - '/shufflepartyparrot', '/skiparrot', '/slomoparrot', '/slowparrot', - '/sovjetparrot', '/stableparrot', '/stalkerparrot', '/tacoparrot', - '/thumbsupparrot', '/tripletsparrot', '/twinsparrot', '/upvotepartyparrot'] - hd_names = [ - '/angelparrot', '/beretparrot', '/birthdaypartyparrot', - '/bluntparrot', '/bunnyparrot', '/calvinist_parrot', - '/ceilingparrot', '/christmasparrot', '/congaparrot', - '/congapartyparrot', '/dealwithitparrot', '/discoparrot', - '/donutparrot', '/fastparrot', '/flyingmoneyparrot', - '/gentlemanparrot', '/hardhatparrot', '/jediparrot', - '/mardigrasparrot', '/marshmallowparrot', '/middleparrot', - '/norwegianblueparrot', '/opensourceparrot', '/parrot', - '/parrotmustache', '/partyparrot', '/pirateparrot', '/popcornparrot', - '/prideparrot', '/pumpkinparrot', '/revolutionparrot', '/rightparrot', - '/sadparrot', '/scienceparrot', '/shuffleparrot', '/sintparrot', - '/sithparrot', '/sushiparrot', '/trans-parrot', '/transparront', - '/ultrafastparrot', '/wendyparrot', '/whitewalkerparrot'] - name += ['/hd%s' % n for n in hd_names] - url = "https://cultofthepartyparrot.com/parrots{}.gif" - try: - return display(Image(url=url.format(random.choice(name)))) - except: - print ':sad_parrot: Looks like the parrot url is not longer there!' - - diff --git a/quantipy/core/helpers/functions.py b/quantipy/core/helpers/functions.py index 51568813f..de3461661 100644 --- a/quantipy/core/helpers/functions.py +++ b/quantipy/core/helpers/functions.py @@ -2569,3 +2569,13 @@ def filtered_set(meta, based_on, masks=True, included=None, excluded=None, def cpickle_copy(obj): copy = cPickle.loads(cPickle.dumps(obj, cPickle.HIGHEST_PROTOCOL)) return copy + +def parrot(): + from IPython.display import Image + from IPython.display import display + import os + filename = os.path.dirname(__file__) + '\\parrot.gif' + try: + return display(Image(filename=filename, format='png')) + except: + print ':sad_parrot: Looks like the parrot is not available!' \ No newline at end of file diff --git a/quantipy/core/helpers/parrot.gif b/quantipy/core/helpers/parrot.gif new file mode 100644 index 0000000000000000000000000000000000000000..b1dbc41b1ddd86b4f85dd49373303161b157d788 GIT binary patch literal 10389 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 a((`@|&xWj9-#j#(3vdGT*%#yqr2Y>+6aGa2 literal 0 HcmV?d00001 From eb38691764044cda5bb99b3df353779cafdd6ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 25 Oct 2018 17:44:35 +0200 Subject: [PATCH 567/733] encoding in filter_names --- quantipy/core/dataset.py | 1 + quantipy/core/stack.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index c7da079c1..46332edd7 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3184,6 +3184,7 @@ def add_filter_var(self, name, logic, overwrite=False): overwrite: bool, default False Overwrite an already existing filter-variable. """ + name = name.encode('utf8') if name in self: if overwrite and not self.is_filter(name): msg = "Cannot add filter-variable '{}', a non-filter" diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 64f7e3f5f..3a4896368 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -801,6 +801,7 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, self[dk][filter_def].data = f_dataset._data self[dk][filter_def].meta = f_dataset._meta fdata = self[dk][filter_def].data + if len(fdata) == 0: raise UserWarning('A filter definition resulted in no cases and will be skipped: {filter_def}'.format(filter_def=filter_def)) continue @@ -1855,6 +1856,7 @@ def _x_y_f_w_map(self, dk, batches='all'): """ """ def _append_loop(mapping, x, fi, w, ys): + fi = fi.encode('utf8') fn = 'no_filter' if fi is None else fi f = 'no_filter' if fi is None else {fi: {fi: 0}} if not x in mapping: @@ -2718,14 +2720,16 @@ def add_tests(self, _batches='all', verbose=True): 'test_total': sigpro.get('test_total', None), 'groups': 'Tests'}) for yy in batch['y_on_y']: - f = ['no_filter'] if y_f[yy] is None else {y_f[yy]: {y_f[yy]: 0}} + fy = y_f[yy].encode('utf8') + f = ['no_filter'] if fy is None else {fy: {fy: 0}} self.add_link(filters=f, x=yks[1:], y=yks, views=vm_tests, weights=weight) total_len = len(x_y) for idx, xy in enumerate(x_y, 1): x, y = xy if x == '@': continue - f = ['no_filter'] if x_f[x] is None else {x_f[x]: {x_f[x]: 0}} + fx = x_f[x].encode('utf8') + f = ['no_filter'] if fx is None else {fx: {fx: 0}} self.add_link(filters=f, x=x, y=y, views=vm_tests, weights=weight) if verbose: From 0fccc198ac0b6245b406eaf6943a56d548e38ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 26 Oct 2018 12:03:56 +0200 Subject: [PATCH 568/733] qp.parrot! --- quantipy/core/helpers/parrot.gif | Bin 10389 -> 15010 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/quantipy/core/helpers/parrot.gif b/quantipy/core/helpers/parrot.gif index b1dbc41b1ddd86b4f85dd49373303161b157d788..458ad859de58f110ed0d2e5caa0619cc901daa28 100644 GIT binary patch literal 15010 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 literal 10389 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 a((`@|&xWj9-#j#(3vdGT*%#yqr2Y>+6aGa2 From e908703713677079c0c6d3ae8f8f95a1a112f39b Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 26 Oct 2018 12:08:11 +0200 Subject: [PATCH 569/733] test fixes --- .pytest_cache/v/cache/nodeids | 4 +- h | 161 ++++++++++++++++++++++++++++++++++ tests/test_batch.py | 28 +++--- tests/test_stack.py | 16 ++-- tests/test_view_manager.py | 4 +- 5 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 h diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index 48d40010d..fc4d323c1 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -3,10 +3,10 @@ "tests/test_banked_chains.py::TestBankedChains::test_banked_chain_structure_weighted", "tests/test_banked_chains.py::TestBankedChains::test_cluster_add_chain", "tests/test_banked_chains.py::TestBankedChains::test_verify_banked_chain", + "tests/test_batch.py::TestBatch::test_add_crossbreak", + "tests/test_batch.py::TestBatch::test_add_downbreak", "tests/test_batch.py::TestBatch::test_add_filter", "tests/test_batch.py::TestBatch::test_add_open_ends", - "tests/test_batch.py::TestBatch::test_add_x", - "tests/test_batch.py::TestBatch::test_add_y", "tests/test_batch.py::TestBatch::test_add_y_on_y", "tests/test_batch.py::TestBatch::test_as_addition", "tests/test_batch.py::TestBatch::test_copy", diff --git a/h b/h new file mode 100644 index 000000000..a4a6582f0 --- /dev/null +++ b/h @@ -0,0 +1,161 @@ +warning: LF will be replaced by CRLF in .pytest_cache/v/cache/nodeids. +The file will have its original line endings in your working directory +diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids +index 48d40010..fc4d323c 100644 +--- a/.pytest_cache/v/cache/nodeids ++++ b/.pytest_cache/v/cache/nodeids +@@ -3,10 +3,10 @@ + "tests/test_banked_chains.py::TestBankedChains::test_banked_chain_structure_weighted",  + "tests/test_banked_chains.py::TestBankedChains::test_cluster_add_chain",  + "tests/test_banked_chains.py::TestBankedChains::test_verify_banked_chain",  ++ "tests/test_batch.py::TestBatch::test_add_crossbreak",  ++ "tests/test_batch.py::TestBatch::test_add_downbreak",  + "tests/test_batch.py::TestBatch::test_add_filter",  + "tests/test_batch.py::TestBatch::test_add_open_ends",  +- "tests/test_batch.py::TestBatch::test_add_x",  +- "tests/test_batch.py::TestBatch::test_add_y",  + "tests/test_batch.py::TestBatch::test_add_y_on_y",  + "tests/test_batch.py::TestBatch::test_as_addition",  + "tests/test_batch.py::TestBatch::test_copy",  +diff --git a/tests/test_batch.py b/tests/test_batch.py +index 6fe7e989..dad3c6b6 100644 +--- a/tests/test_batch.py ++++ b/tests/test_batch.py +@@ -29,8 +29,8 @@ def _get_batch(name, dataset=None, full=False): + if not dataset: dataset = _get_dataset() + batch = qp.Batch(dataset, name) + if full: +- batch.add_x(['q1', 'q2', 'q6', 'age']) +- batch.add_y(['gender', 'q2']) ++ batch.add_downbreak(['q1', 'q2', 'q6', 'age']) ++ batch.add_crossbreak(['gender', 'q2']) + batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') + batch.add_filter('men only', {'gender': 1}) + batch.set_weights('weight_a') +@@ -47,7 +47,7 @@ class TestBatch(unittest.TestCase): + batch1 = dataset.add_batch('batch1') + batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) + self.assertTrue(isinstance(batch1, qp.Batch)) +- self.assertEqual(len(_get_meta(batch1).keys()), 31) ++ self.assertEqual(len(_get_meta(batch1).keys()), 32) + b_meta = _get_meta(batch2) + self.assertEqual(b_meta['name'], 'batch2') + self.assertEqual(b_meta['cell_items'], ['c']) +@@ -76,8 +76,8 @@ class TestBatch(unittest.TestCase): + batch1.hiding('q1', frange('8,9,96-99')) + batch1.slicing('q1', frange('9-4')) + batch2, ds = _get_batch('test2', ds) +- batch2.add_x('q1') +- batch2.add_y('Wave') ++ batch2.add_downbreak('q1') ++ batch2.add_crossbreak('Wave') + batch2.as_addition('test1') + n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') + self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) +@@ -91,9 +91,9 @@ class TestBatch(unittest.TestCase): +  + ########################## methods used in _get_batch #################### +  +- def test_add_x(self): ++ def test_add_downbreak(self): + batch, ds = _get_batch('test') +- batch.add_x(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) ++ batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', + u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', +@@ -115,13 +115,13 @@ class TestBatch(unittest.TestCase): + (u'q14r10c01', ['@'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) +  +- def test_add_y(self): ++ def test_add_crossbreak(self): + batch, ds = _get_batch('test') +- batch.add_y(['gender', 'q2b']) ++ batch.add_crossbreak(['gender', 'q2b']) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) +- self.assertRaises(KeyError, batch.add_y, ['@', 'GENDER']) +- batch.add_x('q1') ++ self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) ++ batch.add_downbreak('q1') + x_y_map = [('q1', ['@', 'gender', 'q2b'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) +  +@@ -143,8 +143,8 @@ class TestBatch(unittest.TestCase): +  + def test_add_filter(self): + batch, ds = _get_batch('test', full=True) +- batch.add_x(['q1', 'q2b']) +- batch.add_y('gender') ++ batch.add_downbreak(['q1', 'q2b']) ++ batch.add_crossbreak('gender') + batch.add_filter('men only', {'gender': 1}) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['filter'], {'men only': {'gender': 1}}) +@@ -213,7 +213,7 @@ class TestBatch(unittest.TestCase): + def test_make_summaries_transpose_arrays(self): + batch, ds = _get_batch('test') + b_meta = _get_meta(batch) +- batch.add_x(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) ++ batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + batch.make_summaries(None) + self.assertEqual(b_meta['summaries'], []) + batch.transpose_arrays(['q5', 'q6'], False) +diff --git a/tests/test_stack.py b/tests/test_stack.py +index e4d3a781..c3e50c83 100644 +--- a/tests/test_stack.py ++++ b/tests/test_stack.py +@@ -1108,17 +1108,17 @@ class TestStackObject(unittest.TestCase): + b1, ds = _get_batch('test1', full=True) + b2, ds = _get_batch('test2', ds, False) + b3, ds = _get_batch('test3', ds, False) +- b1.add_x(['q1', 'q6', 'age']) +- b1.add_y(['gender', 'q2']) ++ b1.add_downbreak(['q1', 'q6', 'age']) ++ b1.add_crossbreak(['gender', 'q2']) + b1.add_filter('men only', {'gender': 1}) + b1.extend_filter({'q1':{'age': [20, 21, 22]}}) + b1.set_weights('weight_a') +- b2.add_x(['q1', 'q6']) +- b2.add_y(['gender', 'q2']) ++ b2.add_downbreak(['q1', 'q6']) ++ b2.add_crossbreak(['gender', 'q2']) + b2.set_weights('weight_b') + b2.transpose_arrays('q6', True) +- b3.add_x(['q1', 'q7']) +- b3.add_y(['q2b']) ++ b3.add_downbreak(['q1', 'q7']) ++ b3.add_crossbreak(['q2b']) + b3.add_y_on_y('y_on_y') + b3.make_summaries(None) + b3.set_weights(['weight_a', 'weight_b']) +@@ -1255,11 +1255,11 @@ class TestStackObject(unittest.TestCase): + '[{}]'.format(v['value'])) for v in values) +  + b1, ds = _get_batch('test1', full=True) +- b1.add_x(['q1', 'q2b', 'q6']) ++ b1.add_downbreak(['q1', 'q2b', 'q6']) + b1.set_variable_text('q1', 'some new text1') + b1.set_variable_text('q6', 'some new text1') + b2, ds = _get_batch('test2', ds, True) +- b2.add_x(['q1', 'q2b', 'q6']) ++ b2.add_downbreak(['q1', 'q2b', 'q6']) + b2.set_variable_text('q1', 'some new text2') + stack = ds.populate(verbose=False) + stack.aggregate(['cbase', 'counts', 'c%'], batches='all', verbose=False) +diff --git a/tests/test_view_manager.py b/tests/test_view_manager.py +index 64c8f914..3ac09f04 100644 +--- a/tests/test_view_manager.py ++++ b/tests/test_view_manager.py +@@ -33,8 +33,8 @@ class TestViewManager(unittest.TestCase): + else: + w = None + batch = dataset.add_batch('viewmanager', weights=w, tests=[0.05] if tests else None) +- batch.add_x(x) +- batch.add_y(y) ++ batch.add_downbreak(x) ++ batch.add_crossbreak(y) + stack = dataset.populate() + basic_views = ['cbase', 'counts', 'c%', 'counts_sum', 'c%_sum'] + stack.aggregate(views=basic_views, verbose=False) diff --git a/tests/test_batch.py b/tests/test_batch.py index 6fe7e9899..dad3c6b60 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -29,8 +29,8 @@ def _get_batch(name, dataset=None, full=False): if not dataset: dataset = _get_dataset() batch = qp.Batch(dataset, name) if full: - batch.add_x(['q1', 'q2', 'q6', 'age']) - batch.add_y(['gender', 'q2']) + batch.add_downbreak(['q1', 'q2', 'q6', 'age']) + batch.add_crossbreak(['gender', 'q2']) batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') batch.add_filter('men only', {'gender': 1}) batch.set_weights('weight_a') @@ -47,7 +47,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 31) + self.assertEqual(len(_get_meta(batch1).keys()), 32) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) @@ -76,8 +76,8 @@ def test_from_batch(self): batch1.hiding('q1', frange('8,9,96-99')) batch1.slicing('q1', frange('9-4')) batch2, ds = _get_batch('test2', ds) - batch2.add_x('q1') - batch2.add_y('Wave') + batch2.add_downbreak('q1') + batch2.add_crossbreak('Wave') batch2.as_addition('test1') n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) @@ -91,9 +91,9 @@ def test_from_batch(self): ########################## methods used in _get_batch #################### - def test_add_x(self): + def test_add_downbreak(self): batch, ds = _get_batch('test') - batch.add_x(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) + batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) b_meta = _get_meta(batch) self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', @@ -115,13 +115,13 @@ def test_add_x(self): (u'q14r10c01', ['@'])] self.assertEqual(b_meta['x_y_map'], x_y_map) - def test_add_y(self): + def test_add_crossbreak(self): batch, ds = _get_batch('test') - batch.add_y(['gender', 'q2b']) + batch.add_crossbreak(['gender', 'q2b']) b_meta = _get_meta(batch) self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) - self.assertRaises(KeyError, batch.add_y, ['@', 'GENDER']) - batch.add_x('q1') + self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) + batch.add_downbreak('q1') x_y_map = [('q1', ['@', 'gender', 'q2b'])] self.assertEqual(b_meta['x_y_map'], x_y_map) @@ -143,8 +143,8 @@ def test_add_open_ends(self): def test_add_filter(self): batch, ds = _get_batch('test', full=True) - batch.add_x(['q1', 'q2b']) - batch.add_y('gender') + batch.add_downbreak(['q1', 'q2b']) + batch.add_crossbreak('gender') batch.add_filter('men only', {'gender': 1}) b_meta = _get_meta(batch) self.assertEqual(b_meta['filter'], {'men only': {'gender': 1}}) @@ -213,7 +213,7 @@ def test_set_sigtest(self): def test_make_summaries_transpose_arrays(self): batch, ds = _get_batch('test') b_meta = _get_meta(batch) - batch.add_x(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) batch.make_summaries(None) self.assertEqual(b_meta['summaries'], []) batch.transpose_arrays(['q5', 'q6'], False) diff --git a/tests/test_stack.py b/tests/test_stack.py index e4d3a7818..c3e50c837 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1108,17 +1108,17 @@ def test_stack_aggregate(self): b1, ds = _get_batch('test1', full=True) b2, ds = _get_batch('test2', ds, False) b3, ds = _get_batch('test3', ds, False) - b1.add_x(['q1', 'q6', 'age']) - b1.add_y(['gender', 'q2']) + b1.add_downbreak(['q1', 'q6', 'age']) + b1.add_crossbreak(['gender', 'q2']) b1.add_filter('men only', {'gender': 1}) b1.extend_filter({'q1':{'age': [20, 21, 22]}}) b1.set_weights('weight_a') - b2.add_x(['q1', 'q6']) - b2.add_y(['gender', 'q2']) + b2.add_downbreak(['q1', 'q6']) + b2.add_crossbreak(['gender', 'q2']) b2.set_weights('weight_b') b2.transpose_arrays('q6', True) - b3.add_x(['q1', 'q7']) - b3.add_y(['q2b']) + b3.add_downbreak(['q1', 'q7']) + b3.add_crossbreak(['q2b']) b3.add_y_on_y('y_on_y') b3.make_summaries(None) b3.set_weights(['weight_a', 'weight_b']) @@ -1255,11 +1255,11 @@ def _factor_on_values(values, axis = 'x'): '[{}]'.format(v['value'])) for v in values) b1, ds = _get_batch('test1', full=True) - b1.add_x(['q1', 'q2b', 'q6']) + b1.add_downbreak(['q1', 'q2b', 'q6']) b1.set_variable_text('q1', 'some new text1') b1.set_variable_text('q6', 'some new text1') b2, ds = _get_batch('test2', ds, True) - b2.add_x(['q1', 'q2b', 'q6']) + b2.add_downbreak(['q1', 'q2b', 'q6']) b2.set_variable_text('q1', 'some new text2') stack = ds.populate(verbose=False) stack.aggregate(['cbase', 'counts', 'c%'], batches='all', verbose=False) diff --git a/tests/test_view_manager.py b/tests/test_view_manager.py index 64c8f914b..3ac09f048 100644 --- a/tests/test_view_manager.py +++ b/tests/test_view_manager.py @@ -33,8 +33,8 @@ def _get_stack(self, unwgt=True, wgt=True, stats=True, nets=True, tests=True): else: w = None batch = dataset.add_batch('viewmanager', weights=w, tests=[0.05] if tests else None) - batch.add_x(x) - batch.add_y(y) + batch.add_downbreak(x) + batch.add_crossbreak(y) stack = dataset.populate() basic_views = ['cbase', 'counts', 'c%', 'counts_sum', 'c%_sum'] stack.aggregate(views=basic_views, verbose=False) From 09ae43de65c0625e64aa76ada1b804932886ee9c Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 26 Oct 2018 12:15:04 +0200 Subject: [PATCH 570/733] fix tests --- tests/test_batch.py | 26 +++++++++++++------------- tests/test_stack.py | 10 +++++----- tests/test_view_manager.py | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test_batch.py b/tests/test_batch.py index 6fe7e9899..6d152fa89 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -29,8 +29,8 @@ def _get_batch(name, dataset=None, full=False): if not dataset: dataset = _get_dataset() batch = qp.Batch(dataset, name) if full: - batch.add_x(['q1', 'q2', 'q6', 'age']) - batch.add_y(['gender', 'q2']) + batch.add_downbreak(['q1', 'q2', 'q6', 'age']) + batch.add_crossbreak(['gender', 'q2']) batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') batch.add_filter('men only', {'gender': 1}) batch.set_weights('weight_a') @@ -76,8 +76,8 @@ def test_from_batch(self): batch1.hiding('q1', frange('8,9,96-99')) batch1.slicing('q1', frange('9-4')) batch2, ds = _get_batch('test2', ds) - batch2.add_x('q1') - batch2.add_y('Wave') + batch2.add_downbreak('q1') + batch2.add_crossbreak('Wave') batch2.as_addition('test1') n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) @@ -91,9 +91,9 @@ def test_from_batch(self): ########################## methods used in _get_batch #################### - def test_add_x(self): + def test_add_downbreak(self): batch, ds = _get_batch('test') - batch.add_x(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) + batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) b_meta = _get_meta(batch) self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', @@ -115,13 +115,13 @@ def test_add_x(self): (u'q14r10c01', ['@'])] self.assertEqual(b_meta['x_y_map'], x_y_map) - def test_add_y(self): + def test_add_crossbreak(self): batch, ds = _get_batch('test') - batch.add_y(['gender', 'q2b']) + batch.add_crossbreak(['gender', 'q2b']) b_meta = _get_meta(batch) self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) - self.assertRaises(KeyError, batch.add_y, ['@', 'GENDER']) - batch.add_x('q1') + self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) + batch.add_downbreak('q1') x_y_map = [('q1', ['@', 'gender', 'q2b'])] self.assertEqual(b_meta['x_y_map'], x_y_map) @@ -143,8 +143,8 @@ def test_add_open_ends(self): def test_add_filter(self): batch, ds = _get_batch('test', full=True) - batch.add_x(['q1', 'q2b']) - batch.add_y('gender') + batch.add_downbreak(['q1', 'q2b']) + batch.add_crossbreak('gender') batch.add_filter('men only', {'gender': 1}) b_meta = _get_meta(batch) self.assertEqual(b_meta['filter'], {'men only': {'gender': 1}}) @@ -213,7 +213,7 @@ def test_set_sigtest(self): def test_make_summaries_transpose_arrays(self): batch, ds = _get_batch('test') b_meta = _get_meta(batch) - batch.add_x(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) batch.make_summaries(None) self.assertEqual(b_meta['summaries'], []) batch.transpose_arrays(['q5', 'q6'], False) diff --git a/tests/test_stack.py b/tests/test_stack.py index e4d3a7818..08a6fcbec 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1108,16 +1108,16 @@ def test_stack_aggregate(self): b1, ds = _get_batch('test1', full=True) b2, ds = _get_batch('test2', ds, False) b3, ds = _get_batch('test3', ds, False) - b1.add_x(['q1', 'q6', 'age']) + b1.add_downbreak(['q1', 'q6', 'age']) b1.add_y(['gender', 'q2']) b1.add_filter('men only', {'gender': 1}) b1.extend_filter({'q1':{'age': [20, 21, 22]}}) b1.set_weights('weight_a') - b2.add_x(['q1', 'q6']) + b2.add_downbreak(['q1', 'q6']) b2.add_y(['gender', 'q2']) b2.set_weights('weight_b') b2.transpose_arrays('q6', True) - b3.add_x(['q1', 'q7']) + b3.add_downbreak(['q1', 'q7']) b3.add_y(['q2b']) b3.add_y_on_y('y_on_y') b3.make_summaries(None) @@ -1255,11 +1255,11 @@ def _factor_on_values(values, axis = 'x'): '[{}]'.format(v['value'])) for v in values) b1, ds = _get_batch('test1', full=True) - b1.add_x(['q1', 'q2b', 'q6']) + b1.add_downbreak(['q1', 'q2b', 'q6']) b1.set_variable_text('q1', 'some new text1') b1.set_variable_text('q6', 'some new text1') b2, ds = _get_batch('test2', ds, True) - b2.add_x(['q1', 'q2b', 'q6']) + b2.add_downbreak(['q1', 'q2b', 'q6']) b2.set_variable_text('q1', 'some new text2') stack = ds.populate(verbose=False) stack.aggregate(['cbase', 'counts', 'c%'], batches='all', verbose=False) diff --git a/tests/test_view_manager.py b/tests/test_view_manager.py index 64c8f914b..3ac09f048 100644 --- a/tests/test_view_manager.py +++ b/tests/test_view_manager.py @@ -33,8 +33,8 @@ def _get_stack(self, unwgt=True, wgt=True, stats=True, nets=True, tests=True): else: w = None batch = dataset.add_batch('viewmanager', weights=w, tests=[0.05] if tests else None) - batch.add_x(x) - batch.add_y(y) + batch.add_downbreak(x) + batch.add_crossbreak(y) stack = dataset.populate() basic_views = ['cbase', 'counts', 'c%', 'counts_sum', 'c%_sum'] stack.aggregate(views=basic_views, verbose=False) From 5318e3835e94cca13163c5c57059e5d2b824e346 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 26 Oct 2018 12:30:56 +0200 Subject: [PATCH 571/733] fix tests --- .pytest_cache/v/cache/nodeids | 4 ++-- tests/test_batch.py | 2 +- tests/test_stack.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index 48d40010d..fc4d323c1 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -3,10 +3,10 @@ "tests/test_banked_chains.py::TestBankedChains::test_banked_chain_structure_weighted", "tests/test_banked_chains.py::TestBankedChains::test_cluster_add_chain", "tests/test_banked_chains.py::TestBankedChains::test_verify_banked_chain", + "tests/test_batch.py::TestBatch::test_add_crossbreak", + "tests/test_batch.py::TestBatch::test_add_downbreak", "tests/test_batch.py::TestBatch::test_add_filter", "tests/test_batch.py::TestBatch::test_add_open_ends", - "tests/test_batch.py::TestBatch::test_add_x", - "tests/test_batch.py::TestBatch::test_add_y", "tests/test_batch.py::TestBatch::test_add_y_on_y", "tests/test_batch.py::TestBatch::test_as_addition", "tests/test_batch.py::TestBatch::test_copy", diff --git a/tests/test_batch.py b/tests/test_batch.py index 6d152fa89..dad3c6b60 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -47,7 +47,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 31) + self.assertEqual(len(_get_meta(batch1).keys()), 32) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) diff --git a/tests/test_stack.py b/tests/test_stack.py index 08a6fcbec..c3e50c837 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1109,16 +1109,16 @@ def test_stack_aggregate(self): b2, ds = _get_batch('test2', ds, False) b3, ds = _get_batch('test3', ds, False) b1.add_downbreak(['q1', 'q6', 'age']) - b1.add_y(['gender', 'q2']) + b1.add_crossbreak(['gender', 'q2']) b1.add_filter('men only', {'gender': 1}) b1.extend_filter({'q1':{'age': [20, 21, 22]}}) b1.set_weights('weight_a') b2.add_downbreak(['q1', 'q6']) - b2.add_y(['gender', 'q2']) + b2.add_crossbreak(['gender', 'q2']) b2.set_weights('weight_b') b2.transpose_arrays('q6', True) b3.add_downbreak(['q1', 'q7']) - b3.add_y(['q2b']) + b3.add_crossbreak(['q2b']) b3.add_y_on_y('y_on_y') b3.make_summaries(None) b3.set_weights(['weight_a', 'weight_b']) From 3938b412efdea7bb692e4545e0762d334ac8153a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 26 Oct 2018 14:25:34 +0200 Subject: [PATCH 572/733] remove file --- h | 161 -------------------------------------------------------------- 1 file changed, 161 deletions(-) delete mode 100644 h diff --git a/h b/h deleted file mode 100644 index a4a6582f0..000000000 --- a/h +++ /dev/null @@ -1,161 +0,0 @@ -warning: LF will be replaced by CRLF in .pytest_cache/v/cache/nodeids. -The file will have its original line endings in your working directory -diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids -index 48d40010..fc4d323c 100644 ---- a/.pytest_cache/v/cache/nodeids -+++ b/.pytest_cache/v/cache/nodeids -@@ -3,10 +3,10 @@ - "tests/test_banked_chains.py::TestBankedChains::test_banked_chain_structure_weighted",  - "tests/test_banked_chains.py::TestBankedChains::test_cluster_add_chain",  - "tests/test_banked_chains.py::TestBankedChains::test_verify_banked_chain",  -+ "tests/test_batch.py::TestBatch::test_add_crossbreak",  -+ "tests/test_batch.py::TestBatch::test_add_downbreak",  - "tests/test_batch.py::TestBatch::test_add_filter",  - "tests/test_batch.py::TestBatch::test_add_open_ends",  -- "tests/test_batch.py::TestBatch::test_add_x",  -- "tests/test_batch.py::TestBatch::test_add_y",  - "tests/test_batch.py::TestBatch::test_add_y_on_y",  - "tests/test_batch.py::TestBatch::test_as_addition",  - "tests/test_batch.py::TestBatch::test_copy",  -diff --git a/tests/test_batch.py b/tests/test_batch.py -index 6fe7e989..dad3c6b6 100644 ---- a/tests/test_batch.py -+++ b/tests/test_batch.py -@@ -29,8 +29,8 @@ def _get_batch(name, dataset=None, full=False): - if not dataset: dataset = _get_dataset() - batch = qp.Batch(dataset, name) - if full: -- batch.add_x(['q1', 'q2', 'q6', 'age']) -- batch.add_y(['gender', 'q2']) -+ batch.add_downbreak(['q1', 'q2', 'q6', 'age']) -+ batch.add_crossbreak(['gender', 'q2']) - batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') - batch.add_filter('men only', {'gender': 1}) - batch.set_weights('weight_a') -@@ -47,7 +47,7 @@ class TestBatch(unittest.TestCase): - batch1 = dataset.add_batch('batch1') - batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) - self.assertTrue(isinstance(batch1, qp.Batch)) -- self.assertEqual(len(_get_meta(batch1).keys()), 31) -+ self.assertEqual(len(_get_meta(batch1).keys()), 32) - b_meta = _get_meta(batch2) - self.assertEqual(b_meta['name'], 'batch2') - self.assertEqual(b_meta['cell_items'], ['c']) -@@ -76,8 +76,8 @@ class TestBatch(unittest.TestCase): - batch1.hiding('q1', frange('8,9,96-99')) - batch1.slicing('q1', frange('9-4')) - batch2, ds = _get_batch('test2', ds) -- batch2.add_x('q1') -- batch2.add_y('Wave') -+ batch2.add_downbreak('q1') -+ batch2.add_crossbreak('Wave') - batch2.as_addition('test1') - n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') - self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) -@@ -91,9 +91,9 @@ class TestBatch(unittest.TestCase): -  - ########################## methods used in _get_batch #################### -  -- def test_add_x(self): -+ def test_add_downbreak(self): - batch, ds = _get_batch('test') -- batch.add_x(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) -+ batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', - u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', -@@ -115,13 +115,13 @@ class TestBatch(unittest.TestCase): - (u'q14r10c01', ['@'])] - self.assertEqual(b_meta['x_y_map'], x_y_map) -  -- def test_add_y(self): -+ def test_add_crossbreak(self): - batch, ds = _get_batch('test') -- batch.add_y(['gender', 'q2b']) -+ batch.add_crossbreak(['gender', 'q2b']) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) -- self.assertRaises(KeyError, batch.add_y, ['@', 'GENDER']) -- batch.add_x('q1') -+ self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) -+ batch.add_downbreak('q1') - x_y_map = [('q1', ['@', 'gender', 'q2b'])] - self.assertEqual(b_meta['x_y_map'], x_y_map) -  -@@ -143,8 +143,8 @@ class TestBatch(unittest.TestCase): -  - def test_add_filter(self): - batch, ds = _get_batch('test', full=True) -- batch.add_x(['q1', 'q2b']) -- batch.add_y('gender') -+ batch.add_downbreak(['q1', 'q2b']) -+ batch.add_crossbreak('gender') - batch.add_filter('men only', {'gender': 1}) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['filter'], {'men only': {'gender': 1}}) -@@ -213,7 +213,7 @@ class TestBatch(unittest.TestCase): - def test_make_summaries_transpose_arrays(self): - batch, ds = _get_batch('test') - b_meta = _get_meta(batch) -- batch.add_x(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) -+ batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) - batch.make_summaries(None) - self.assertEqual(b_meta['summaries'], []) - batch.transpose_arrays(['q5', 'q6'], False) -diff --git a/tests/test_stack.py b/tests/test_stack.py -index e4d3a781..c3e50c83 100644 ---- a/tests/test_stack.py -+++ b/tests/test_stack.py -@@ -1108,17 +1108,17 @@ class TestStackObject(unittest.TestCase): - b1, ds = _get_batch('test1', full=True) - b2, ds = _get_batch('test2', ds, False) - b3, ds = _get_batch('test3', ds, False) -- b1.add_x(['q1', 'q6', 'age']) -- b1.add_y(['gender', 'q2']) -+ b1.add_downbreak(['q1', 'q6', 'age']) -+ b1.add_crossbreak(['gender', 'q2']) - b1.add_filter('men only', {'gender': 1}) - b1.extend_filter({'q1':{'age': [20, 21, 22]}}) - b1.set_weights('weight_a') -- b2.add_x(['q1', 'q6']) -- b2.add_y(['gender', 'q2']) -+ b2.add_downbreak(['q1', 'q6']) -+ b2.add_crossbreak(['gender', 'q2']) - b2.set_weights('weight_b') - b2.transpose_arrays('q6', True) -- b3.add_x(['q1', 'q7']) -- b3.add_y(['q2b']) -+ b3.add_downbreak(['q1', 'q7']) -+ b3.add_crossbreak(['q2b']) - b3.add_y_on_y('y_on_y') - b3.make_summaries(None) - b3.set_weights(['weight_a', 'weight_b']) -@@ -1255,11 +1255,11 @@ class TestStackObject(unittest.TestCase): - '[{}]'.format(v['value'])) for v in values) -  - b1, ds = _get_batch('test1', full=True) -- b1.add_x(['q1', 'q2b', 'q6']) -+ b1.add_downbreak(['q1', 'q2b', 'q6']) - b1.set_variable_text('q1', 'some new text1') - b1.set_variable_text('q6', 'some new text1') - b2, ds = _get_batch('test2', ds, True) -- b2.add_x(['q1', 'q2b', 'q6']) -+ b2.add_downbreak(['q1', 'q2b', 'q6']) - b2.set_variable_text('q1', 'some new text2') - stack = ds.populate(verbose=False) - stack.aggregate(['cbase', 'counts', 'c%'], batches='all', verbose=False) -diff --git a/tests/test_view_manager.py b/tests/test_view_manager.py -index 64c8f914..3ac09f04 100644 ---- a/tests/test_view_manager.py -+++ b/tests/test_view_manager.py -@@ -33,8 +33,8 @@ class TestViewManager(unittest.TestCase): - else: - w = None - batch = dataset.add_batch('viewmanager', weights=w, tests=[0.05] if tests else None) -- batch.add_x(x) -- batch.add_y(y) -+ batch.add_downbreak(x) -+ batch.add_crossbreak(y) - stack = dataset.populate() - basic_views = ['cbase', 'counts', 'c%', 'counts_sum', 'c%_sum'] - stack.aggregate(views=basic_views, verbose=False) From f23654c65d247baca030def88839848d750cdd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 26 Oct 2018 14:57:48 +0200 Subject: [PATCH 573/733] adjust gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 21c7716d8..09411b85a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,6 @@ docs/API/_templates/ *.egg .eggs/ .cache -.pytest_cache/ +*.pytest_cache/ build From 0dcb32dc6d0bc5f95999da7f08b5913cb3c1e796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 26 Oct 2018 15:28:41 +0200 Subject: [PATCH 574/733] gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 09411b85a..e188fdab4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ docs/API/_templates/ *.egg .eggs/ .cache -*.pytest_cache/ +.pytest_cache/ +.pytest_cache/v/cache/nodeids build From 78167511ecfc793a2e309c4b6a80453002489bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 29 Oct 2018 11:03:47 +0100 Subject: [PATCH 575/733] subset in weight --- quantipy/core/dataset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 4eead17f6..15972d219 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2809,6 +2809,11 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', the weight factors. """ if subset: + if isinstance(subset, basestring): + if self.is_filter(subset): + subset = {subset: 0} + else: + raise ValueError('{} is not a valid filter_var'.format(subset)) ds = self.filter('subset', subset, False) meta, data = ds.split() else: From 9f8f0f9036a5118661127a744f1cd00528ab0be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 29 Oct 2018 12:24:13 +0100 Subject: [PATCH 576/733] adjustments for filters --- quantipy/core/batch.py | 2266 +++++++++++++++++++------------------- quantipy/core/dataset.py | 8 +- tests/test_batch.py | 664 +++++------ 3 files changed, 1471 insertions(+), 1467 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 6114ddd20..59a7cfa0e 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1,1133 +1,1133 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -import numpy as np -import pandas as pd -import quantipy as qp - -from collections import OrderedDict - -from quantipy.core.tools.qp_decorators import * - -import copy as org_copy -import warnings -import re - -from quantipy.core.tools.view.logic import ( - has_any, has_all, has_count, - not_any, not_all, not_count, - is_lt, is_ne, is_gt, - is_le, is_eq, is_ge, - union, intersection, get_logic_index) - -def meta_editor(self, dataset_func): - """ - Decorator for inherited DataSet methods. - """ - def edit(*args, **kwargs): - # get name and type of the variable dor correct dict refernces - name = args[0] if args else kwargs['name'] - if not isinstance(name, list): name = [name] - # create DataSet clone to leave global meta data untouched - if self.edits_ds is None: - self.edits_ds = qp.DataSet.clone(self) - ds_clone = self.edits_ds - var_edits = [] - # args/ kwargs for min_value_count - if dataset_func.func_name == 'min_value_count': - if len(args) < 3 and not 'weight' in kwargs: - kwargs['weight'] = self.weights[0] - - if len(args) < 4 and not 'condition' in kwargs: - if not self.filter == 'no_filter': - kwargs['condition'] = self.filter.values()[0] - # args/ kwargs for sorting - elif dataset_func.func_name == 'sorting': - if len(args) < 7 and not 'sort_by_weight' in kwargs: - kwargs['sort_by_weight'] = self.weights[0] - - for n in name: - is_array = self.is_array(n) - is_array_item = self._is_array_item(n) - has_edits = n in self.meta_edits - parent = self._maskname_from_item(n) if is_array_item else None - parent_edits = parent in self.meta_edits - source = self.sources(n) if is_array else [] - source_edits = [s in self.meta_edits for s in source] - # are we adding to aleady existing batch meta edits? (use copy then!) - var_edits += [(n, has_edits), (parent, parent_edits)] - var_edits += [(s, s_edit) for s, s_edit in zip(source, source_edits)] - for var, edits in var_edits: - if edits: - copied_meta = org_copy.deepcopy(self.meta_edits[var]) - if not self.is_array(var): - ds_clone._meta['columns'][var] = copied_meta - else: - ds_clone._meta['masks'][var] = copied_meta - if self.meta_edits['lib'].get(var): - lib = self.meta_edits['lib'][var] - ds_clone._meta['lib']['values'][var] = lib - # use qp.DataSet method to apply the edit - dataset_func(ds_clone, *args, **kwargs) - # grab edited meta data and collect via Batch.meta_edits attribute - for n in self.unroll(name, both='all'): - if not self.is_array(n): - meta = ds_clone._meta['columns'][n] - text_edits = ['set_col_text_edit', 'set_val_text_edit'] - if dataset_func.func_name in text_edits and is_array_item: - self.meta_edits[parent] = ds_clone._meta['masks'][parent] - lib = ds_clone._meta['lib']['values'][parent] - self.meta_edits['lib'][parent] = lib - else: - meta = ds_clone._meta['masks'][n] - if ds_clone._has_categorical_data(n): - self.meta_edits['lib'][n] = ds_clone._meta['lib']['values'][n] - self.meta_edits[n] = meta - if dataset_func.func_name in ['hiding', 'slicing', 'min_value_count', 'sorting']: - self._update() - return edit - -def not_implemented(dataset_func): - """ - Decorator for UNALLOWED DataSet methods. - """ - def _unallowed_inherited_method(*args, **kwargs): - err_msg = 'DataSet method not allowed for Batch editing!' - raise NotImplementedError(err_msg) - return _unallowed_inherited_method - - -class Batch(qp.DataSet): - """ - A Batch is a container for structuring a Link collection's - specifications aimed at Excel and/or PPTX build Clusters. - """ - def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): - if '-' in name: raise ValueError("Batch 'name' must not contain '-'!") - sets = dataset._meta['sets'] - if not 'batches' in sets: sets['batches'] = OrderedDict() - self.name = name - meta, data = dataset.split() - self._meta = meta - self._data = data - self.edits_ds = None - self.valid_tks = dataset.valid_tks - self.text_key = dataset.text_key - self.sample_size = None - self._verbose_errors = dataset._verbose_errors - self._verbose_infos = dataset._verbose_infos - self._dimensions_comp = dataset._dimensions_comp - - # RENAMED DataSet methods - self._dsfilter = qp.DataSet.filter.__func__ - - if sets['batches'].get(name): - if self._verbose_infos: - print "Load Batch '{}'.".format(name) - self._load_batch() - else: - sets['batches'][name] = {'name': name, 'additions': []} - self.xks = [] - self.yks = ['@'] - self._variables = [] - self.total = True - self.extended_yks_global = None - self.extended_yks_per_x = {} - self.exclusive_yks_per_x = {} - self.extended_filters_per_x = {} - self.filter = None - self.filter_names = [] - self.x_y_map = None - self.x_filter_map = None - self.y_on_y = [] - self.y_filter_map = {} - self.forced_names = {} - self.summaries = [] - self.transposed_arrays = {} - self.skip_items = [] - self.verbatims = [] - # self.verbatim_names = [] - self.set_cell_items(ci) # self.cell_items - self.unwgt_counts = False - self.set_weights(weights) # self.weights - self.set_sigtests(tests) # self.sigproperties - self.additional = False - self.meta_edits = {'lib': {}} - self.build_info = {} - self.set_language(dataset.text_key) # self.language - self._update() - - # DECORATED / OVERWRITTEN DataSet methods - # self.hide_empty_items = meta_editor(self, qp.DataSet.hide_empty_items.__func__) - self.hiding = meta_editor(self, qp.DataSet.hiding.__func__) - self.min_value_count = meta_editor(self, qp.DataSet.min_value_count.__func__) - self.sorting = meta_editor(self, qp.DataSet.sorting.__func__) - self.slicing = meta_editor(self, qp.DataSet.slicing.__func__) - self.set_variable_text = meta_editor(self, qp.DataSet.set_variable_text.__func__) - self.set_value_texts = meta_editor(self, qp.DataSet.set_value_texts.__func__) - self.set_property = meta_editor(self, qp.DataSet.set_property.__func__) - # UNALLOWED DataSet methods - # self.add_meta = not_implemented(qp.DataSet.add_meta.__func__) - self.derive = not_implemented(qp.DataSet.derive.__func__) - self.remove_items = not_implemented(qp.DataSet.remove_items.__func__) - self.set_missings = not_implemented(qp.DataSet.set_missings.__func__) - - def _update(self): - """ - Update Batch metadata with Batch attributes. - """ - self._map_x_to_y() - self._map_x_to_filter() - self._samplesize_from_batch_filter() - attrs = self.__dict__ - for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'name', 'skip_items', 'total', - 'unwgt_counts', 'y_filter_map', 'build_info', - ]: - attr_update = {attr: attrs.get(attr, attrs.get('_{}'.format(attr)))} - self._meta['sets']['batches'][self.name].update(attr_update) - - def _load_batch(self): - """ - Fill batch attributes with information from meta. - """ - bdefs = self._meta['sets']['batches'][self.name] - for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', - 'y_filter_map', 'build_info', - ]: - attr_load = {attr: bdefs.get(attr, bdefs.get('_{}'.format(attr)))} - self.__dict__.update(attr_load) - - def clone(self, name, b_filter=None, as_addition=False): - """ - Create a copy of Batch instance. - - Parameters - ---------- - name: str - Name of the Batch instance that is copied. - b_filter: tuple (str, dict/ complex logic) - Filter logic which is applied on the new batch. - (filtername, filterlogic) - as_addition: bool, default False - If True, the new batch is added as addition to the master batch. - - Returns - ------- - New/ copied Batch instance. - """ - org_name = self.name - org_meta = org_copy.deepcopy(self._meta['sets']['batches'][org_name]) - self._meta['sets']['batches'][name] = org_meta - verbose = self._verbose_infos - self.set_verbose_infomsg(False) - batch_copy = self.get_batch(name) - self.set_verbose_infomsg(verbose) - batch_copy.set_verbose_infomsg(verbose) - if b_filter: - batch_copy.add_filter(b_filter[0], b_filter[1]) - if batch_copy.verbatims and b_filter and not as_addition: - for oe in batch_copy.verbatims: - data = self._data.copy() - series_data = data['@1'].copy()[pd.Index(oe['idx'])] - slicer, _ = get_logic_index(series_data, b_filter[1], data) - oe['idx'] = slicer.tolist() - if as_addition: - batch_copy.as_addition(self.name) - batch_copy._update() - return batch_copy - - def remove(self): - """ - Remove instance from meta object. - """ - name = self.name - adds = self._meta['sets']['batches'][name]['additions'] - if adds: - for bname, bdef in self._meta['sets']['batches'].items(): - if bname == name: continue - for add in adds[:]: - if add in bdef['additions']: - adds.remove(add) - for add in adds: - self._meta['sets']['batches'][add]['additional'] = False - - del(self._meta['sets']['batches'][name]) - if self._verbose_infos: - print "Batch '%s' is removed from meta-object." % name - self = None - return None - - def _rename_in_additions(self, find_bname, new_name): - for bname, bdef in self._meta['sets']['batches'].items(): - if find_bname in bdef['additions']: - adds = bdef['additions'] - adds[adds.index(find_bname)] = new_name - bdef['additions'] = adds - return None - - def rename(self, new_name): - """ - Rename instance, updating ``DataSet`` references to the definiton, too. - """ - if new_name in self._meta['sets']['batches']: - raise KeyError("'%s' is already included!" % new_name) - batches = self._meta['sets']['batches'] - org_name = self.name - batches[new_name] = batches.pop(org_name) - self._rename_in_additions(org_name, new_name) - self.name = new_name - self._update() - return None - - @modify(to_list='ci') - def set_cell_items(self, ci): - """ - Assign cell items ('c', 'p', 'cp'). - - Parameters - ---------- - ci: str/ list of str, {'c', 'p', 'cp'} - Cell items used for this Batch instance. - - Returns - ------- - None - """ - if any(c not in ['c', 'p', 'cp'] for c in ci): - raise ValueError("'ci' cell items must be either 'c', 'p' or 'cp'.") - self.cell_items = ci - self._update() - return None - - def set_unwgt_counts(self, unwgt=False): - """ - Assign if counts (incl. nets) should be aggregated unweighted. - """ - self.unwgt_counts = unwgt - self._update() - return None - - @modify(to_list='w') - def set_weights(self, w): - """ - Assign a weight variable setup. - - Parameters - ---------- - w: str/ list of str - Name(s) of the weight variable(s). - - Returns - ------- - None - """ - if not w: - w = [None] - elif any(we is None for we in w): - w = [None] + [we for we in w if not we is None] - self.weights = w - if any(weight not in self.columns() for weight in w if not weight is None): - raise ValueError('{} is not in DataSet.'.format(w)) - self._update() - return None - - @modify(to_list='levels') - def set_sigtests(self, levels=None, flags=[30, 100], test_total=False, mimic=None): - """ - Specify a significance test setup. - - Parameters - ---------- - levels: float/ list of float - Level(s) for significance calculation(s). - mimic/ flags/ test_total: - Currently not implemented. - - Returns - ------- - None - """ - if levels and self.total: - if not all(isinstance(l, float) for l in levels): - raise TypeError('All significance levels must be provided as floats!') - levels = sorted(levels) - else: - levels = [] - - self.sigproperties = {'siglevels': levels, - 'test_total': test_total, - 'flag_bases': flags, - 'mimic': ['Dim']} - if mimic : - err = ("Changes to 'mimic' are currently not allowed!") - raise NotImplementedError(err) - self._update() - return None - - @verify(text_keys='text_key') - def set_language(self, text_key): - """ - Set ``Batch.language`` indicated via the ``text_key`` for Build exports. - - Parameters - ---------- - text_key: str - The text_key used as language for the Batch instance - - Returns - ------- - None - """ - self.language = text_key - self._update() - return None - - def as_addition(self, batch_name): - """ - Treat the Batch as additional aggregations, independent from the - global Batch & Build setup. - - Parameters - ---------- - batch_name: str - Name of the Batch instance where the current instance is added to. - - Returns - ------- - None - """ - self._meta['sets']['batches'][batch_name]['additions'].append(self.name) - self.additional = True - self.verbatims = [] - self.y_on_y = [] - self.y_filter_map = {} - if self._verbose_infos: - msg = ("Batch '{}' specified as addition to Batch '{}'. Any open end " - "summaries and 'y_on_y' agg. have been removed!") - print msg.format(self.name, batch_name) - self._update() - return None - - - @modify(to_list='varlist') - def add_variables(self, varlist): - """ - Text - - Parameters - ---------- - varlist : list - A list of variable names. - - Returns - ------- - None - """ - self._variables = [] - if '@' in varlist: varlist.remove('@') - if '@1' in varlist: varlist.remove('@1') - for v in varlist: - if not v in self._variables: - self._variables.append(v) - self._update() - return None - - - @modify(to_list='dbrk') - def add_downbreak(self, dbrk): - """ - Set the downbreak (x) variables of the Batch. - - Parameters - ---------- - dbrk: str, list of str, dict, list of dict - Names of variables that are used as downbreaks. Forced names for - Excel outputs can be given in a dict, for example: - xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] - - Returns - ------- - None - """ - clean_xks = self._check_forced_names(dbrk) - self.xks = self.unroll(clean_xks, both='all') - self._update() - masks = [x for x in self.xks if x in self.masks()] - self.make_summaries(masks, [], _verbose=False) - return None - - - @modify(to_list='xks') - def add_x(self, xks): - """ - Set the x (downbreak) variables of the Batch. - - Parameters - ---------- - xks: str, list of str, dict, list of dict - Names of variables that are used as downbreaks. Forced names for - Excel outputs can be given in a dict, for example: - xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] - - Returns - ------- - None - """ - w = "'add_x()' will be deprecated in a future version. Please use 'add_downbreak()' instead!" - warnings.warn(w) - self.add_downbreak(xks) - - @modify(to_list=['ext_xks']) - def extend_x(self, ext_xks): - """ - Extend downbreak variables with additional variables. - - Parameters - ---------- - ext_xks: str/ dict, list of str/dict - Name(s) of variable(s) that are added as downbreak. If a dict is - provided, the variable is added in front of the belonging key. - Example:: - >>> ext_xks = ['var1', {'existing_x': ['var2', 'var3']}] - - var1 is added at the end of the downbreaks, var2 and var3 are - added in front of the variable existing_x. - - Returns - ------- - None - """ - for x in ext_xks: - if isinstance(x, dict): - for pos, var in x.items(): - if not isinstance(var, list): var = [var] - var = self.unroll(var, both='all') - for v in var: - if not self.var_exists(pos): - raise KeyError('{} is not included.'.format(pos)) - elif not v in self.xks: - self.xks.insert(self.xks.index(pos), v) - if self.is_array(v) and not v in self.summaries: - self.summaries.append(v) - elif not self.var_exists(x): - raise KeyError('{} is not included.'.format(x)) - elif x not in self.xks: - self.xks.extend(self.unroll(x, both='all')) - if self.is_array(x) and not x in self.summaries: - self.summaries.append(x) - self._update() - return None - - def hide_empty(self, xks=True, summaries=True): - """ - Drop empty variables and hide array items from summaries. - - Parametes - --------- - xks : bool, default True - Controls dropping "regular" variables and array items due to being - empty. - summaries : bool, default True - Controls whether or not empty array items are hidden (by applying - rules) in summary aggregations. Summaries that would end up with - no valid items are automatically dropped altogether. - - Returns - ------- - None - """ - if self.filter == 'no_filter': - cond = None - else: - cond = self.filter.values()[0] - removed_sum = [] - for x in self.xks[:]: - if self.is_array(x): - e_items = self.empty_items(x, cond, False) - if not e_items: continue - sources = self.sources(x) - if summaries: - self.hiding(x, e_items, axis='x', hide_values=False) - if len(e_items) == len(sources): - if x in self.xks: self.xks.remove(x) - if x in self.summaries: self.summaries.remove(x) - removed_sum.append(x) - if xks: - for i in e_items: - if sources[i-1] in self.xks: - self.xks.remove(sources[i-1]) - elif not self._is_array_item(x): - if self[self.take(cond), x].count() == 0: - self.xks.remove(x) - if removed_sum: - msg = "Dropping summaries for {} - all items hidden!" - warnings.warn(msg.format(removed_sum)) - self._update() - return None - - @modify(to_list=['arrays']) - @verify(variables={'arrays': 'masks'}) - def make_summaries(self, arrays, exclusive=False, _verbose=None): - """ - Summary tables are created for defined arrays. - - Parameters - ---------- - arrays: str/ list of str - List of arrays for which summary tables are created. Summary tables - can only be created for arrays that are included in ``self.xks``. - exclusive: bool/ list, default False - If True only summaries are created and items skipped. ``exclusive`` - parameter can be provided for a selection of arrays. Example:: - >>> b.make_summaries(['array1', 'array2'], exclusive = ['array2']) - Returns - ------- - None - """ - if _verbose is None: _verbose = self._verbose_infos - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - self.summaries = arrays - if exclusive: - if isinstance(exclusive, bool): - self.skip_items = arrays - else: - self.skip_items = [a for a in exclusive if a in arrays] - else: - self.skip_items = [] - if arrays: - msg = 'Array summaries setup: Creating {}.'.format(arrays) - else: - msg = 'Array summaries setup: Creating no summaries!' - if _verbose: - print msg - for t_array in self.transposed_arrays.keys(): - if not t_array in arrays: - self.transposed_arrays.pop(t_array) - self._update() - return None - - @modify(to_list='arrays') - @verify(variables={'arrays': 'masks'}) - def transpose_arrays(self, arrays, replace=False): - """ - Transposed summary tables are created for defined arrays. - - Parameters - ---------- - arrays: str/ list of str - List of arrays for which transposed summary tables are created. - Transposed summary tables can only be created for arrays that are - included in ``self.xks``. - replace: bool, default True - If True only the transposed table is created, if False transposed - and normal summary tables are created. - - Returns - ------- - None - """ - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - if any(a not in self.summaries for a in arrays): - ar = list(set(self.summaries + arrays)) - a = [v for v in self.xks if v in ar] - self.make_summaries(a, []) - for array in arrays: - self.transposed_arrays[array] = replace - self._update() - return None - - def add_total(self, total=True): - """ - Define if '@' is added to y_keys. - """ - if not total: - self.set_sigtests(None) - if self._verbose_infos: - print 'sigtests are removed from batch.' - self.total = total - self.add_crossbreak(self.yks) - return None - - @modify(to_list='xbrk') - @verify(variables={'xbrk': 'both'}, categorical='xbrk') - def add_crossbreak(self, xbrk): - """ - Set the y (crossbreak/banner) variables of the Batch. - - Parameters - ---------- - xbrk: str, list of str - Variables that are added as crossbreaks. '@'/ total is added - automatically. - - Returns - ------- - None - """ - yks = [y for y in xbrk if not y=='@'] - yks = self.unroll(yks) - if self.total: - yks = ['@'] + yks - self.yks = yks - self._update() - return None - - @modify(to_list='yks') - @verify(variables={'yks': 'both'}, categorical='yks') - def add_y(self, yks): - """ - Set the y (crossbreak/banner) variables of the Batch. - - Parameters - ---------- - yks: str, list of str - Variables that are added as crossbreaks. '@'/ total is added - automatically. - - Returns - ------- - None - """ - w = "'add_y()' will be deprecated in a future version. Please use 'add_crossbreak()' instead!" - warnings.warn(w) - self.add_crossbreak(yks) - - def add_x_per_y(self, x_on_y_map): - """ - Add individual combinations of x and y variables to the Batch. - - !!! Currently not implemented !!! - """ - raise NotImplementedError('NOT YET SUPPPORTED') - if not isinstance(x_on_y_map, list): x_on_y_maps = [x_on_y_map] - if not isinstance(x_on_y_maps[0], dict): - raise TypeError('Must pass a (list of) dicts!') - for x_on_y_map in x_on_y_maps: - for x, y in x_on_y_map.items(): - if not isinstance(y, list): y = [y] - if isinstance(x, tuple): x = {x[0]: x[1]} - return None - - def add_filter(self, filter_name, filter_logic=None, overwrite=False): - """ - Apply a (global) filter to all the variables found in the Batch. - - Parameters - ---------- - filter_name: str - Name for the added filter. - filter_logic: complex logic - Logic for the added filter. - - Returns - ------- - None - """ - name = filter_name.encode('utf-8', errors='ignore') - if self.is_filter(name): - if not (filter_logic is None or overwrite): - raise ValueError("'{}' is already a filter-variable. Cannot " - "apply a new logic.".format(name)) - elif overwrite: - self.drop(name) - print 'Overwrite filter var: {}'.format(name) - self.add_filter_var(name, filter_logic, overwrite) - - else: - self.add_filter_var(name, filter_logic, overwrite) - self.filter = name - if not name in self.filter_names: - self.filter_names.append(name) - self._update() - return None - - def remove_filter(self): - """ - Remove all defined (global + extended) filters from Batch. - """ - self.filter = None - self.filter_names = [] - self.extended_filters_per_x = {} - self._update() - return None - - @modify(to_list=['oe', 'break_by', 'title']) - @verify(variables={'oe': 'columns', 'break_by': 'columns'}) - def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, - replacements=None, split=False, title='open ends', - filter_by=None, overwrite=True): - """ - Create respondent level based listings of open-ended text data. - - Parameters - ---------- - oe : str or list of str - The open-ended questions / verbatims to be added to the stack. - break_by : str or list of str, default None - If provided, these variables will be presented alongside the ``oe`` - data. - drop_empty : bool, default True - Case data that is missing valid entries will be dropped from the - output. - incl_nan: bool, default False - Show __NaN__ in the output. - replacements: dict, default None - Replace strings in data. - split: bool, default False - If True len of oe must be same size as len of title. Each oe is - saved with its own title. - title : str, default 'open ends' - Specifies the the ``Cluster`` / Excel sheet name for the output. - filter_by : A Quantipy logical expression, default None - An additional logical filter that should be applied to the case data. - Any ``filter`` provided by a ``batch`` will be respected - automatically. - overwrite : bool, default False - If True and used title is already existing in self.verbatims, then - it gets overwritten - - Returns - ------- - None - """ - if self.additional: - err_msg = "Cannot add open end DataFrames to as_addition()-Batches!" - raise NotImplementedError(err_msg) - dupes = [v for v in oe if v in break_by] - if dupes: - raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) - def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): - if filter_by: - if self.filter: - f_name = '{}_{}'.format(self.filter, title) - else: - f_name = '{}_f'.format(title) - if self.is_filter(f_name): - logic = intersection([{self.filter: 0}, filter_by]) - if not self.take(logic).index.tolist() == self.manifest_filter(f_name): - msg = "'{}' is already in use with an other logic." - raise ValueError(msg.format(f_name)) - else: - logic = {'label': title, 'logic': filter_by} - if self.filter: - self.extend_filter_var(self.filter, logic, title) - else: - self.add_filter_var(f_name, logic) - slicer = f_name - else: - slicer = self.filter - if any(oe['title'] == title for oe in self.verbatims) and not overwrite: - return None - oe = { - 'title': title, - 'filter': slicer, - 'columns': oe, - 'break_by': break_by, - 'incl_nan': incl_nan, - 'drop_empty': drop_empty, - 'replace': replacements} - if any(o['title'] == title for o in self.verbatims): - for x, o in enumerate(self.verbatims): - if o['title'] == title: - self.verbatims[x] = oe - else: - self.verbatims.append(oe) - - if len(oe) + len(break_by) == 0: - raise ValueError("Please add any variables as 'oe' or 'break_by'.") - if split: - if not len(oe) == len(title): - msg = "Cannot derive verbatim DataFrame 'title' with more than 1 'oe'" - raise ValueError(msg) - for t, open_end in zip(title, oe): - open_end = [open_end] - _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite) - else: - _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite) - self._update() - return None - - @modify(to_list=['ext_yks', 'on']) - @verify(variables={'ext_yks': 'columns'}) - def extend_y(self, ext_yks, on=None): - """ - Add y (crossbreak/banner) variables to specific x (downbreak) variables. - - Parameters - ---------- - ext_yks: str/ dict, list of str/ dict - Name(s) of variable(s) that are added as crossbreak. If a dict is - provided, the variable is added in front of the beloning key. - Example:: - >>> ext_yks = ['var1', {'existing_y': ['var2', 'var3']}] - - var1 is added at the end of the crossbreaks, var2 and var3 are - added in front of the variable existing_y. - on: str/ list of str - Name(s) of variable(s) in the xks (downbreaks) for which the - crossbreak should be extended. - - Returns - ------- - None - """ - ext_yks = [e for e in ext_yks if not e in self.yks] - if not on: - self.yks.extend(ext_yks) - if not self.extended_yks_global: - self.extended_yks_global = ext_yks - else: - self.extended_yks_global.extend(ext_yks) - else: - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) - raise ValueError(msg) - on = self.unroll(on, both='all') - for x in on: - x_ext_yks = [e for e in ext_yks - if not e in self.extended_yks_per_x.get(x, [])] - self.extended_yks_per_x.update({x: x_ext_yks}) - self._update() - return None - - @modify(to_list=['new_yks', 'on']) - @verify(variables={'new_yks': 'both', 'on': 'both'}) - def replace_y(self, new_yks, on): - """ - Replace y (crossbreak/banner) variables on specific x (downbreak) variables. - - Parameters - ---------- - ext_yks: str/ list of str - Name(s) of variable(s) that are used as crossbreak. - on: str/ list of str - Name(s) of variable(s) in the xks (downbreaks) for which the - crossbreak should be replaced. - - Returns - ------- - None - """ - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) - raise ValueError(msg) - on = self.unroll(on, both='all') - if not '@' in new_yks: new_yks = ['@'] + new_yks - for x in on: - self.exclusive_yks_per_x.update({x: new_yks}) - self._update() - return None - - def extend_filter(self, ext_filters): - """ - Apply additonal filtering to specific x (downbreak) variables. - - Parameters - ---------- - ext_filters: dict - dict with variable name(s) as key, str or tupel of str, and logic - as value. For example: - ext_filters = {'q1': {'gender': 1}, ('q2', 'q3'): {'gender': 2}} - - Returns - ------- - None - """ - for variables, logic in ext_filters.items(): - if not isinstance(variables, (list, tuple)): - variables = [variables] - for v in variables: - if self.filter: - log = {'label': v, 'logic': logic} - f_name = '{}_{}'.format(self.filter, v) - self.extend_filter_var(self.filter, log, v) - else: - f_name = '{}_f'.format(v) - self.add_filter_var(f_name, log) - self.extended_filters_per_x.update({v: f_name}) - self._update() - return None - - def add_y_on_y(self, name, y_filter=None, main_filter='extend'): - """ - Produce aggregations crossing the (main) y variables with each other. - - Parameters - ---------- - name: str - key name for the y on y aggregation. - y_filter: dict (complex logic), default None - Add a filter for the y on y aggregation. If None is provided - the main batch filter is taken. - main_filter: {'extend', 'replace'}, default 'extend' - Defines if the main batch filter is extended or - replaced by the y_on_y filter. - - In order to remove all filters from the y on y aggregation set - ``y_filter='no_filter'`` and ``main_filter='replace'``. - - Returns - ------- - None - """ - if not isinstance(name, basestring): - raise TypeError("'name' attribute for add_y_on_y must be a str!") - elif not main_filter in ['extend', 'replace'] or main_filter is None: - raise ValueError("'main_filter' must be either 'extend' or 'replace'.") - if not name in self.y_on_y: - self.y_on_y.append(name) - if y_filter is not None: - logic = {'label': name, 'logic': y_filter} - if main_filter == 'extend': - if self.filter: - f_name = '{}_{}'.format(self.filter, name) - self.extend_filter_var(self.filter, logic, name) - else: - f_name = '{}_f'.format(name) - self.add_filter_var(f_name, logic) - elif main_filter == 'replace': - f_name = '{}_f'.format(name) - self.add_filter_var(f_name, logic) - else: - if main_filter == 'replace': - f_name = None - else: - f_name = self.filter - - self.y_filter_map[name] = f_name - self._update() - return None - - def _map_x_to_y(self): - """ - Combine all defined cross and downbreaks in a map. - - Returns - ------- - None - """ - def _order_yks(yks): - y_keys = [] - for y in yks: - if isinstance(y, dict): - for pos, var in y.items(): - if not isinstance(var, list): var = [var] - for v in var: - if not v in y_keys: - y_keys.insert(y_keys.index(pos), v) - elif not y in y_keys: - y_keys.append(y) - return y_keys - - def _get_yks(x): - if x in self.exclusive_yks_per_x: - yks = self.exclusive_yks_per_x[x] - else: - yks = org_copy.deepcopy(self.yks) - yks.extend(self.extended_yks_per_x.get(x, [])) - yks = _order_yks(yks) - return yks - - mapping = [] - for x in self.xks: - if self.is_array(x): - if x in self.summaries and not self.transposed_arrays.get(x): - mapping.append((x, ['@'])) - if not x in self.skip_items: - try: - hiding = self.meta_edits[x]['rules']['x']['dropx']['values'] - except: - hiding = self._get_rules(x).get('dropx', {}).get('values', []) - for x2 in self.sources(x): - if x2 in hiding: - continue - elif x2 in self.xks: - mapping.append((x2, _get_yks(x2))) - if x in self.transposed_arrays: - mapping.append(('@', [x])) - elif self._is_array_item(x) and self._maskname_from_item(x) in self.xks: - continue - else: - mapping.append((x, _get_yks(x))) - self.x_y_map = mapping - return None - - def _map_x_to_filter(self): - """ - Combine all defined downbreaks with its beloning filter in a map. - - Returns - ------- - None - """ - mapping = {} - for x in self.xks: - if self._is_array_item(x): - continue - name = self.extended_filters_per_x.get(x, self.filter) - mapping[x] = name - if self.is_array(x): - for x2 in self.sources(x): - if x2 in self.xks: - mapping[x2] = name - if name and not name in self.filter_names: - self.filter_names.append(name) - self.x_filter_map = mapping - return None - - def _check_forced_names(self, variables): - """ - Store forced names for xks and return adjusted list of downbreaks. - - Parameters - ---------- - variables: list of str/dict/tuple - Variables that are checked. If a dict or tupel is provided, the - key/ first item is used as variable name and the value/ second - item as forced name. - - Returns - ------- - xks: list of str - """ - xks = [] - renames = {} - for x in variables: - if isinstance(x, dict): - xks.append(x.keys()[0]) - renames[x.keys()[0]] = x.values()[0] - elif isinstance(x, tuple): - xks.append(x[0]) - renames[x[0]] = x[1] - else: - xks.append(x) - if not self.var_exists(xks[-1]): - raise ValueError('{} is not in DataSet.'.format(xks[-1])) - self.forced_names = renames - return xks - - def _samplesize_from_batch_filter(self): - """ - Calculates sample_size from existing filter. - """ - if self.filter: - idx = self.manifest_filter(self.filter) - else: - idx = self._data.index - self.sample_size = len(idx) - return None +#!/usr/bin/python +# -*- coding: utf-8 -*- +import numpy as np +import pandas as pd +import quantipy as qp + +from collections import OrderedDict + +from quantipy.core.tools.qp_decorators import * + +import copy as org_copy +import warnings +import re + +from quantipy.core.tools.view.logic import ( + has_any, has_all, has_count, + not_any, not_all, not_count, + is_lt, is_ne, is_gt, + is_le, is_eq, is_ge, + union, intersection, get_logic_index) + +def meta_editor(self, dataset_func): + """ + Decorator for inherited DataSet methods. + """ + def edit(*args, **kwargs): + # get name and type of the variable dor correct dict refernces + name = args[0] if args else kwargs['name'] + if not isinstance(name, list): name = [name] + # create DataSet clone to leave global meta data untouched + if self.edits_ds is None: + self.edits_ds = qp.DataSet.clone(self) + ds_clone = self.edits_ds + var_edits = [] + # args/ kwargs for min_value_count + if dataset_func.func_name == 'min_value_count': + if len(args) < 3 and not 'weight' in kwargs: + kwargs['weight'] = self.weights[0] + + if len(args) < 4 and not 'condition' in kwargs: + if not self.filter == 'no_filter': + kwargs['condition'] = self.filter.values()[0] + # args/ kwargs for sorting + elif dataset_func.func_name == 'sorting': + if len(args) < 7 and not 'sort_by_weight' in kwargs: + kwargs['sort_by_weight'] = self.weights[0] + + for n in name: + is_array = self.is_array(n) + is_array_item = self._is_array_item(n) + has_edits = n in self.meta_edits + parent = self._maskname_from_item(n) if is_array_item else None + parent_edits = parent in self.meta_edits + source = self.sources(n) if is_array else [] + source_edits = [s in self.meta_edits for s in source] + # are we adding to aleady existing batch meta edits? (use copy then!) + var_edits += [(n, has_edits), (parent, parent_edits)] + var_edits += [(s, s_edit) for s, s_edit in zip(source, source_edits)] + for var, edits in var_edits: + if edits: + copied_meta = org_copy.deepcopy(self.meta_edits[var]) + if not self.is_array(var): + ds_clone._meta['columns'][var] = copied_meta + else: + ds_clone._meta['masks'][var] = copied_meta + if self.meta_edits['lib'].get(var): + lib = self.meta_edits['lib'][var] + ds_clone._meta['lib']['values'][var] = lib + # use qp.DataSet method to apply the edit + dataset_func(ds_clone, *args, **kwargs) + # grab edited meta data and collect via Batch.meta_edits attribute + for n in self.unroll(name, both='all'): + if not self.is_array(n): + meta = ds_clone._meta['columns'][n] + text_edits = ['set_col_text_edit', 'set_val_text_edit'] + if dataset_func.func_name in text_edits and is_array_item: + self.meta_edits[parent] = ds_clone._meta['masks'][parent] + lib = ds_clone._meta['lib']['values'][parent] + self.meta_edits['lib'][parent] = lib + else: + meta = ds_clone._meta['masks'][n] + if ds_clone._has_categorical_data(n): + self.meta_edits['lib'][n] = ds_clone._meta['lib']['values'][n] + self.meta_edits[n] = meta + if dataset_func.func_name in ['hiding', 'slicing', 'min_value_count', 'sorting']: + self._update() + return edit + +def not_implemented(dataset_func): + """ + Decorator for UNALLOWED DataSet methods. + """ + def _unallowed_inherited_method(*args, **kwargs): + err_msg = 'DataSet method not allowed for Batch editing!' + raise NotImplementedError(err_msg) + return _unallowed_inherited_method + + +class Batch(qp.DataSet): + """ + A Batch is a container for structuring a Link collection's + specifications aimed at Excel and/or PPTX build Clusters. + """ + def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): + if '-' in name: raise ValueError("Batch 'name' must not contain '-'!") + sets = dataset._meta['sets'] + if not 'batches' in sets: sets['batches'] = OrderedDict() + self.name = name + meta, data = dataset.split() + self._meta = meta + self._data = data + self.edits_ds = None + self.valid_tks = dataset.valid_tks + self.text_key = dataset.text_key + self.sample_size = None + self._verbose_errors = dataset._verbose_errors + self._verbose_infos = dataset._verbose_infos + self._dimensions_comp = dataset._dimensions_comp + + # RENAMED DataSet methods + self._dsfilter = qp.DataSet.filter.__func__ + + if sets['batches'].get(name): + if self._verbose_infos: + print "Load Batch '{}'.".format(name) + self._load_batch() + else: + sets['batches'][name] = {'name': name, 'additions': []} + self.xks = [] + self.yks = ['@'] + self._variables = [] + self.total = True + self.extended_yks_global = None + self.extended_yks_per_x = {} + self.exclusive_yks_per_x = {} + self.extended_filters_per_x = {} + self.filter = None + self.filter_names = [] + self.x_y_map = None + self.x_filter_map = None + self.y_on_y = [] + self.y_filter_map = {} + self.forced_names = {} + self.summaries = [] + self.transposed_arrays = {} + self.skip_items = [] + self.verbatims = [] + # self.verbatim_names = [] + self.set_cell_items(ci) # self.cell_items + self.unwgt_counts = False + self.set_weights(weights) # self.weights + self.set_sigtests(tests) # self.sigproperties + self.additional = False + self.meta_edits = {'lib': {}} + self.build_info = {} + self.set_language(dataset.text_key) # self.language + self._update() + + # DECORATED / OVERWRITTEN DataSet methods + # self.hide_empty_items = meta_editor(self, qp.DataSet.hide_empty_items.__func__) + self.hiding = meta_editor(self, qp.DataSet.hiding.__func__) + self.min_value_count = meta_editor(self, qp.DataSet.min_value_count.__func__) + self.sorting = meta_editor(self, qp.DataSet.sorting.__func__) + self.slicing = meta_editor(self, qp.DataSet.slicing.__func__) + self.set_variable_text = meta_editor(self, qp.DataSet.set_variable_text.__func__) + self.set_value_texts = meta_editor(self, qp.DataSet.set_value_texts.__func__) + self.set_property = meta_editor(self, qp.DataSet.set_property.__func__) + # UNALLOWED DataSet methods + # self.add_meta = not_implemented(qp.DataSet.add_meta.__func__) + self.derive = not_implemented(qp.DataSet.derive.__func__) + self.remove_items = not_implemented(qp.DataSet.remove_items.__func__) + self.set_missings = not_implemented(qp.DataSet.set_missings.__func__) + + def _update(self): + """ + Update Batch metadata with Batch attributes. + """ + self._map_x_to_y() + self._map_x_to_filter() + self._samplesize_from_batch_filter() + attrs = self.__dict__ + for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', + 'x_y_map', 'x_filter_map', 'y_on_y', + 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', + 'extended_yks_global', 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language', 'name', 'skip_items', 'total', + 'unwgt_counts', 'y_filter_map', 'build_info', + ]: + attr_update = {attr: attrs.get(attr, attrs.get('_{}'.format(attr)))} + self._meta['sets']['batches'][self.name].update(attr_update) + + def _load_batch(self): + """ + Fill batch attributes with information from meta. + """ + bdefs = self._meta['sets']['batches'][self.name] + for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', + 'x_y_map', 'x_filter_map', 'y_on_y', + 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', + 'extended_yks_global', 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', + 'y_filter_map', 'build_info', + ]: + attr_load = {attr: bdefs.get(attr, bdefs.get('_{}'.format(attr)))} + self.__dict__.update(attr_load) + + def clone(self, name, b_filter=None, as_addition=False): + """ + Create a copy of Batch instance. + + Parameters + ---------- + name: str + Name of the Batch instance that is copied. + b_filter: tuple (str, dict/ complex logic) + Filter logic which is applied on the new batch. + (filtername, filterlogic) + as_addition: bool, default False + If True, the new batch is added as addition to the master batch. + + Returns + ------- + New/ copied Batch instance. + """ + org_name = self.name + org_meta = org_copy.deepcopy(self._meta['sets']['batches'][org_name]) + self._meta['sets']['batches'][name] = org_meta + verbose = self._verbose_infos + self.set_verbose_infomsg(False) + batch_copy = self.get_batch(name) + self.set_verbose_infomsg(verbose) + batch_copy.set_verbose_infomsg(verbose) + if b_filter: + batch_copy.add_filter(b_filter[0], b_filter[1]) + if batch_copy.verbatims and b_filter and not as_addition: + for oe in batch_copy.verbatims: + data = self._data.copy() + series_data = data['@1'].copy()[pd.Index(oe['idx'])] + slicer, _ = get_logic_index(series_data, b_filter[1], data) + oe['idx'] = slicer.tolist() + if as_addition: + batch_copy.as_addition(self.name) + batch_copy._update() + return batch_copy + + def remove(self): + """ + Remove instance from meta object. + """ + name = self.name + adds = self._meta['sets']['batches'][name]['additions'] + if adds: + for bname, bdef in self._meta['sets']['batches'].items(): + if bname == name: continue + for add in adds[:]: + if add in bdef['additions']: + adds.remove(add) + for add in adds: + self._meta['sets']['batches'][add]['additional'] = False + + del(self._meta['sets']['batches'][name]) + if self._verbose_infos: + print "Batch '%s' is removed from meta-object." % name + self = None + return None + + def _rename_in_additions(self, find_bname, new_name): + for bname, bdef in self._meta['sets']['batches'].items(): + if find_bname in bdef['additions']: + adds = bdef['additions'] + adds[adds.index(find_bname)] = new_name + bdef['additions'] = adds + return None + + def rename(self, new_name): + """ + Rename instance, updating ``DataSet`` references to the definiton, too. + """ + if new_name in self._meta['sets']['batches']: + raise KeyError("'%s' is already included!" % new_name) + batches = self._meta['sets']['batches'] + org_name = self.name + batches[new_name] = batches.pop(org_name) + self._rename_in_additions(org_name, new_name) + self.name = new_name + self._update() + return None + + @modify(to_list='ci') + def set_cell_items(self, ci): + """ + Assign cell items ('c', 'p', 'cp'). + + Parameters + ---------- + ci: str/ list of str, {'c', 'p', 'cp'} + Cell items used for this Batch instance. + + Returns + ------- + None + """ + if any(c not in ['c', 'p', 'cp'] for c in ci): + raise ValueError("'ci' cell items must be either 'c', 'p' or 'cp'.") + self.cell_items = ci + self._update() + return None + + def set_unwgt_counts(self, unwgt=False): + """ + Assign if counts (incl. nets) should be aggregated unweighted. + """ + self.unwgt_counts = unwgt + self._update() + return None + + @modify(to_list='w') + def set_weights(self, w): + """ + Assign a weight variable setup. + + Parameters + ---------- + w: str/ list of str + Name(s) of the weight variable(s). + + Returns + ------- + None + """ + if not w: + w = [None] + elif any(we is None for we in w): + w = [None] + [we for we in w if not we is None] + self.weights = w + if any(weight not in self.columns() for weight in w if not weight is None): + raise ValueError('{} is not in DataSet.'.format(w)) + self._update() + return None + + @modify(to_list='levels') + def set_sigtests(self, levels=None, flags=[30, 100], test_total=False, mimic=None): + """ + Specify a significance test setup. + + Parameters + ---------- + levels: float/ list of float + Level(s) for significance calculation(s). + mimic/ flags/ test_total: + Currently not implemented. + + Returns + ------- + None + """ + if levels and self.total: + if not all(isinstance(l, float) for l in levels): + raise TypeError('All significance levels must be provided as floats!') + levels = sorted(levels) + else: + levels = [] + + self.sigproperties = {'siglevels': levels, + 'test_total': test_total, + 'flag_bases': flags, + 'mimic': ['Dim']} + if mimic : + err = ("Changes to 'mimic' are currently not allowed!") + raise NotImplementedError(err) + self._update() + return None + + @verify(text_keys='text_key') + def set_language(self, text_key): + """ + Set ``Batch.language`` indicated via the ``text_key`` for Build exports. + + Parameters + ---------- + text_key: str + The text_key used as language for the Batch instance + + Returns + ------- + None + """ + self.language = text_key + self._update() + return None + + def as_addition(self, batch_name): + """ + Treat the Batch as additional aggregations, independent from the + global Batch & Build setup. + + Parameters + ---------- + batch_name: str + Name of the Batch instance where the current instance is added to. + + Returns + ------- + None + """ + self._meta['sets']['batches'][batch_name]['additions'].append(self.name) + self.additional = True + self.verbatims = [] + self.y_on_y = [] + self.y_filter_map = {} + if self._verbose_infos: + msg = ("Batch '{}' specified as addition to Batch '{}'. Any open end " + "summaries and 'y_on_y' agg. have been removed!") + print msg.format(self.name, batch_name) + self._update() + return None + + + @modify(to_list='varlist') + def add_variables(self, varlist): + """ + Text + + Parameters + ---------- + varlist : list + A list of variable names. + + Returns + ------- + None + """ + self._variables = [] + if '@' in varlist: varlist.remove('@') + if '@1' in varlist: varlist.remove('@1') + for v in varlist: + if not v in self._variables: + self._variables.append(v) + self._update() + return None + + + @modify(to_list='dbrk') + def add_downbreak(self, dbrk): + """ + Set the downbreak (x) variables of the Batch. + + Parameters + ---------- + dbrk: str, list of str, dict, list of dict + Names of variables that are used as downbreaks. Forced names for + Excel outputs can be given in a dict, for example: + xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] + + Returns + ------- + None + """ + clean_xks = self._check_forced_names(dbrk) + self.xks = self.unroll(clean_xks, both='all') + self._update() + masks = [x for x in self.xks if x in self.masks()] + self.make_summaries(masks, [], _verbose=False) + return None + + + @modify(to_list='xks') + def add_x(self, xks): + """ + Set the x (downbreak) variables of the Batch. + + Parameters + ---------- + xks: str, list of str, dict, list of dict + Names of variables that are used as downbreaks. Forced names for + Excel outputs can be given in a dict, for example: + xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] + + Returns + ------- + None + """ + w = "'add_x()' will be deprecated in a future version. Please use 'add_downbreak()' instead!" + warnings.warn(w) + self.add_downbreak(xks) + + @modify(to_list=['ext_xks']) + def extend_x(self, ext_xks): + """ + Extend downbreak variables with additional variables. + + Parameters + ---------- + ext_xks: str/ dict, list of str/dict + Name(s) of variable(s) that are added as downbreak. If a dict is + provided, the variable is added in front of the belonging key. + Example:: + >>> ext_xks = ['var1', {'existing_x': ['var2', 'var3']}] + + var1 is added at the end of the downbreaks, var2 and var3 are + added in front of the variable existing_x. + + Returns + ------- + None + """ + for x in ext_xks: + if isinstance(x, dict): + for pos, var in x.items(): + if not isinstance(var, list): var = [var] + var = self.unroll(var, both='all') + for v in var: + if not self.var_exists(pos): + raise KeyError('{} is not included.'.format(pos)) + elif not v in self.xks: + self.xks.insert(self.xks.index(pos), v) + if self.is_array(v) and not v in self.summaries: + self.summaries.append(v) + elif not self.var_exists(x): + raise KeyError('{} is not included.'.format(x)) + elif x not in self.xks: + self.xks.extend(self.unroll(x, both='all')) + if self.is_array(x) and not x in self.summaries: + self.summaries.append(x) + self._update() + return None + + def hide_empty(self, xks=True, summaries=True): + """ + Drop empty variables and hide array items from summaries. + + Parametes + --------- + xks : bool, default True + Controls dropping "regular" variables and array items due to being + empty. + summaries : bool, default True + Controls whether or not empty array items are hidden (by applying + rules) in summary aggregations. Summaries that would end up with + no valid items are automatically dropped altogether. + + Returns + ------- + None + """ + if self.filter == 'no_filter': + cond = None + else: + cond = self.filter.values()[0] + removed_sum = [] + for x in self.xks[:]: + if self.is_array(x): + e_items = self.empty_items(x, cond, False) + if not e_items: continue + sources = self.sources(x) + if summaries: + self.hiding(x, e_items, axis='x', hide_values=False) + if len(e_items) == len(sources): + if x in self.xks: self.xks.remove(x) + if x in self.summaries: self.summaries.remove(x) + removed_sum.append(x) + if xks: + for i in e_items: + if sources[i-1] in self.xks: + self.xks.remove(sources[i-1]) + elif not self._is_array_item(x): + if self[self.take(cond), x].count() == 0: + self.xks.remove(x) + if removed_sum: + msg = "Dropping summaries for {} - all items hidden!" + warnings.warn(msg.format(removed_sum)) + self._update() + return None + + @modify(to_list=['arrays']) + @verify(variables={'arrays': 'masks'}) + def make_summaries(self, arrays, exclusive=False, _verbose=None): + """ + Summary tables are created for defined arrays. + + Parameters + ---------- + arrays: str/ list of str + List of arrays for which summary tables are created. Summary tables + can only be created for arrays that are included in ``self.xks``. + exclusive: bool/ list, default False + If True only summaries are created and items skipped. ``exclusive`` + parameter can be provided for a selection of arrays. Example:: + >>> b.make_summaries(['array1', 'array2'], exclusive = ['array2']) + Returns + ------- + None + """ + if _verbose is None: _verbose = self._verbose_infos + if any(a not in self.xks for a in arrays): + msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) + raise ValueError(msg) + self.summaries = arrays + if exclusive: + if isinstance(exclusive, bool): + self.skip_items = arrays + else: + self.skip_items = [a for a in exclusive if a in arrays] + else: + self.skip_items = [] + if arrays: + msg = 'Array summaries setup: Creating {}.'.format(arrays) + else: + msg = 'Array summaries setup: Creating no summaries!' + if _verbose: + print msg + for t_array in self.transposed_arrays.keys(): + if not t_array in arrays: + self.transposed_arrays.pop(t_array) + self._update() + return None + + @modify(to_list='arrays') + @verify(variables={'arrays': 'masks'}) + def transpose_arrays(self, arrays, replace=False): + """ + Transposed summary tables are created for defined arrays. + + Parameters + ---------- + arrays: str/ list of str + List of arrays for which transposed summary tables are created. + Transposed summary tables can only be created for arrays that are + included in ``self.xks``. + replace: bool, default True + If True only the transposed table is created, if False transposed + and normal summary tables are created. + + Returns + ------- + None + """ + if any(a not in self.xks for a in arrays): + msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) + raise ValueError(msg) + if any(a not in self.summaries for a in arrays): + ar = list(set(self.summaries + arrays)) + a = [v for v in self.xks if v in ar] + self.make_summaries(a, []) + for array in arrays: + self.transposed_arrays[array] = replace + self._update() + return None + + def add_total(self, total=True): + """ + Define if '@' is added to y_keys. + """ + if not total: + self.set_sigtests(None) + if self._verbose_infos: + print 'sigtests are removed from batch.' + self.total = total + self.add_crossbreak(self.yks) + return None + + @modify(to_list='xbrk') + @verify(variables={'xbrk': 'both'}, categorical='xbrk') + def add_crossbreak(self, xbrk): + """ + Set the y (crossbreak/banner) variables of the Batch. + + Parameters + ---------- + xbrk: str, list of str + Variables that are added as crossbreaks. '@'/ total is added + automatically. + + Returns + ------- + None + """ + yks = [y for y in xbrk if not y=='@'] + yks = self.unroll(yks) + if self.total: + yks = ['@'] + yks + self.yks = yks + self._update() + return None + + @modify(to_list='yks') + @verify(variables={'yks': 'both'}, categorical='yks') + def add_y(self, yks): + """ + Set the y (crossbreak/banner) variables of the Batch. + + Parameters + ---------- + yks: str, list of str + Variables that are added as crossbreaks. '@'/ total is added + automatically. + + Returns + ------- + None + """ + w = "'add_y()' will be deprecated in a future version. Please use 'add_crossbreak()' instead!" + warnings.warn(w) + self.add_crossbreak(yks) + + def add_x_per_y(self, x_on_y_map): + """ + Add individual combinations of x and y variables to the Batch. + + !!! Currently not implemented !!! + """ + raise NotImplementedError('NOT YET SUPPPORTED') + if not isinstance(x_on_y_map, list): x_on_y_maps = [x_on_y_map] + if not isinstance(x_on_y_maps[0], dict): + raise TypeError('Must pass a (list of) dicts!') + for x_on_y_map in x_on_y_maps: + for x, y in x_on_y_map.items(): + if not isinstance(y, list): y = [y] + if isinstance(x, tuple): x = {x[0]: x[1]} + return None + + def add_filter(self, filter_name, filter_logic=None, overwrite=False): + """ + Apply a (global) filter to all the variables found in the Batch. + + Parameters + ---------- + filter_name: str + Name for the added filter. + filter_logic: complex logic + Logic for the added filter. + + Returns + ------- + None + """ + name = filter_name.encode('utf8').replace(' ', '_').replace('~', '_') + if self.is_filter(name): + if not (filter_logic is None or overwrite): + raise ValueError("'{}' is already a filter-variable. Cannot " + "apply a new logic.".format(name)) + elif overwrite: + self.drop(name) + print 'Overwrite filter var: {}'.format(name) + self.add_filter_var(name, filter_logic, overwrite) + + else: + self.add_filter_var(name, filter_logic, overwrite) + self.filter = name + if not name in self.filter_names: + self.filter_names.append(name) + self._update() + return None + + def remove_filter(self): + """ + Remove all defined (global + extended) filters from Batch. + """ + self.filter = None + self.filter_names = [] + self.extended_filters_per_x = {} + self._update() + return None + + @modify(to_list=['oe', 'break_by', 'title']) + @verify(variables={'oe': 'columns', 'break_by': 'columns'}) + def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, + replacements=None, split=False, title='open ends', + filter_by=None, overwrite=True): + """ + Create respondent level based listings of open-ended text data. + + Parameters + ---------- + oe : str or list of str + The open-ended questions / verbatims to be added to the stack. + break_by : str or list of str, default None + If provided, these variables will be presented alongside the ``oe`` + data. + drop_empty : bool, default True + Case data that is missing valid entries will be dropped from the + output. + incl_nan: bool, default False + Show __NaN__ in the output. + replacements: dict, default None + Replace strings in data. + split: bool, default False + If True len of oe must be same size as len of title. Each oe is + saved with its own title. + title : str, default 'open ends' + Specifies the the ``Cluster`` / Excel sheet name for the output. + filter_by : A Quantipy logical expression, default None + An additional logical filter that should be applied to the case data. + Any ``filter`` provided by a ``batch`` will be respected + automatically. + overwrite : bool, default False + If True and used title is already existing in self.verbatims, then + it gets overwritten + + Returns + ------- + None + """ + if self.additional: + err_msg = "Cannot add open end DataFrames to as_addition()-Batches!" + raise NotImplementedError(err_msg) + dupes = [v for v in oe if v in break_by] + if dupes: + raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) + def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): + if filter_by: + if self.filter: + f_name = '{}_{}'.format(self.filter, title) + else: + f_name = '{}_f'.format(title) + if self.is_filter(f_name): + logic = intersection([{self.filter: 0}, filter_by]) + if not self.take(logic).index.tolist() == self.manifest_filter(f_name): + msg = "'{}' is already in use with an other logic." + raise ValueError(msg.format(f_name)) + else: + logic = {'label': title, 'logic': filter_by} + if self.filter: + self.extend_filter_var(self.filter, logic, title) + else: + self.add_filter_var(f_name, logic) + slicer = f_name + else: + slicer = self.filter + if any(oe['title'] == title for oe in self.verbatims) and not overwrite: + return None + oe = { + 'title': title, + 'filter': slicer, + 'columns': oe, + 'break_by': break_by, + 'incl_nan': incl_nan, + 'drop_empty': drop_empty, + 'replace': replacements} + if any(o['title'] == title for o in self.verbatims): + for x, o in enumerate(self.verbatims): + if o['title'] == title: + self.verbatims[x] = oe + else: + self.verbatims.append(oe) + + if len(oe) + len(break_by) == 0: + raise ValueError("Please add any variables as 'oe' or 'break_by'.") + if split: + if not len(oe) == len(title): + msg = "Cannot derive verbatim DataFrame 'title' with more than 1 'oe'" + raise ValueError(msg) + for t, open_end in zip(title, oe): + open_end = [open_end] + _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite) + else: + _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite) + self._update() + return None + + @modify(to_list=['ext_yks', 'on']) + @verify(variables={'ext_yks': 'columns'}) + def extend_y(self, ext_yks, on=None): + """ + Add y (crossbreak/banner) variables to specific x (downbreak) variables. + + Parameters + ---------- + ext_yks: str/ dict, list of str/ dict + Name(s) of variable(s) that are added as crossbreak. If a dict is + provided, the variable is added in front of the beloning key. + Example:: + >>> ext_yks = ['var1', {'existing_y': ['var2', 'var3']}] + + var1 is added at the end of the crossbreaks, var2 and var3 are + added in front of the variable existing_y. + on: str/ list of str + Name(s) of variable(s) in the xks (downbreaks) for which the + crossbreak should be extended. + + Returns + ------- + None + """ + ext_yks = [e for e in ext_yks if not e in self.yks] + if not on: + self.yks.extend(ext_yks) + if not self.extended_yks_global: + self.extended_yks_global = ext_yks + else: + self.extended_yks_global.extend(ext_yks) + else: + if any(o not in self.xks for o in on): + msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + raise ValueError(msg) + on = self.unroll(on, both='all') + for x in on: + x_ext_yks = [e for e in ext_yks + if not e in self.extended_yks_per_x.get(x, [])] + self.extended_yks_per_x.update({x: x_ext_yks}) + self._update() + return None + + @modify(to_list=['new_yks', 'on']) + @verify(variables={'new_yks': 'both', 'on': 'both'}) + def replace_y(self, new_yks, on): + """ + Replace y (crossbreak/banner) variables on specific x (downbreak) variables. + + Parameters + ---------- + ext_yks: str/ list of str + Name(s) of variable(s) that are used as crossbreak. + on: str/ list of str + Name(s) of variable(s) in the xks (downbreaks) for which the + crossbreak should be replaced. + + Returns + ------- + None + """ + if any(o not in self.xks for o in on): + msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + raise ValueError(msg) + on = self.unroll(on, both='all') + if not '@' in new_yks: new_yks = ['@'] + new_yks + for x in on: + self.exclusive_yks_per_x.update({x: new_yks}) + self._update() + return None + + def extend_filter(self, ext_filters): + """ + Apply additonal filtering to specific x (downbreak) variables. + + Parameters + ---------- + ext_filters: dict + dict with variable name(s) as key, str or tupel of str, and logic + as value. For example: + ext_filters = {'q1': {'gender': 1}, ('q2', 'q3'): {'gender': 2}} + + Returns + ------- + None + """ + for variables, logic in ext_filters.items(): + if not isinstance(variables, (list, tuple)): + variables = [variables] + for v in variables: + if self.filter: + log = {'label': v, 'logic': logic} + f_name = '{}_{}'.format(self.filter, v) + self.extend_filter_var(self.filter, log, v) + else: + f_name = '{}_f'.format(v) + self.add_filter_var(f_name, log) + self.extended_filters_per_x.update({v: f_name}) + self._update() + return None + + def add_y_on_y(self, name, y_filter=None, main_filter='extend'): + """ + Produce aggregations crossing the (main) y variables with each other. + + Parameters + ---------- + name: str + key name for the y on y aggregation. + y_filter: dict (complex logic), default None + Add a filter for the y on y aggregation. If None is provided + the main batch filter is taken. + main_filter: {'extend', 'replace'}, default 'extend' + Defines if the main batch filter is extended or + replaced by the y_on_y filter. + + In order to remove all filters from the y on y aggregation set + ``y_filter='no_filter'`` and ``main_filter='replace'``. + + Returns + ------- + None + """ + if not isinstance(name, basestring): + raise TypeError("'name' attribute for add_y_on_y must be a str!") + elif not main_filter in ['extend', 'replace'] or main_filter is None: + raise ValueError("'main_filter' must be either 'extend' or 'replace'.") + if not name in self.y_on_y: + self.y_on_y.append(name) + if y_filter is not None: + logic = {'label': name, 'logic': y_filter} + if main_filter == 'extend': + if self.filter: + f_name = '{}_{}'.format(self.filter, name) + self.extend_filter_var(self.filter, logic, name) + else: + f_name = '{}_f'.format(name) + self.add_filter_var(f_name, logic) + elif main_filter == 'replace': + f_name = '{}_f'.format(name) + self.add_filter_var(f_name, logic) + else: + if main_filter == 'replace': + f_name = None + else: + f_name = self.filter + + self.y_filter_map[name] = f_name + self._update() + return None + + def _map_x_to_y(self): + """ + Combine all defined cross and downbreaks in a map. + + Returns + ------- + None + """ + def _order_yks(yks): + y_keys = [] + for y in yks: + if isinstance(y, dict): + for pos, var in y.items(): + if not isinstance(var, list): var = [var] + for v in var: + if not v in y_keys: + y_keys.insert(y_keys.index(pos), v) + elif not y in y_keys: + y_keys.append(y) + return y_keys + + def _get_yks(x): + if x in self.exclusive_yks_per_x: + yks = self.exclusive_yks_per_x[x] + else: + yks = org_copy.deepcopy(self.yks) + yks.extend(self.extended_yks_per_x.get(x, [])) + yks = _order_yks(yks) + return yks + + mapping = [] + for x in self.xks: + if self.is_array(x): + if x in self.summaries and not self.transposed_arrays.get(x): + mapping.append((x, ['@'])) + if not x in self.skip_items: + try: + hiding = self.meta_edits[x]['rules']['x']['dropx']['values'] + except: + hiding = self._get_rules(x).get('dropx', {}).get('values', []) + for x2 in self.sources(x): + if x2 in hiding: + continue + elif x2 in self.xks: + mapping.append((x2, _get_yks(x2))) + if x in self.transposed_arrays: + mapping.append(('@', [x])) + elif self._is_array_item(x) and self._maskname_from_item(x) in self.xks: + continue + else: + mapping.append((x, _get_yks(x))) + self.x_y_map = mapping + return None + + def _map_x_to_filter(self): + """ + Combine all defined downbreaks with its beloning filter in a map. + + Returns + ------- + None + """ + mapping = {} + for x in self.xks: + if self._is_array_item(x): + continue + name = self.extended_filters_per_x.get(x, self.filter) + mapping[x] = name + if self.is_array(x): + for x2 in self.sources(x): + if x2 in self.xks: + mapping[x2] = name + if name and not name in self.filter_names: + self.filter_names.append(name) + self.x_filter_map = mapping + return None + + def _check_forced_names(self, variables): + """ + Store forced names for xks and return adjusted list of downbreaks. + + Parameters + ---------- + variables: list of str/dict/tuple + Variables that are checked. If a dict or tupel is provided, the + key/ first item is used as variable name and the value/ second + item as forced name. + + Returns + ------- + xks: list of str + """ + xks = [] + renames = {} + for x in variables: + if isinstance(x, dict): + xks.append(x.keys()[0]) + renames[x.keys()[0]] = x.values()[0] + elif isinstance(x, tuple): + xks.append(x[0]) + renames[x[0]] = x[1] + else: + xks.append(x) + if not self.var_exists(xks[-1]): + raise ValueError('{} is not in DataSet.'.format(xks[-1])) + self.forced_names = renames + return xks + + def _samplesize_from_batch_filter(self): + """ + Calculates sample_size from existing filter. + """ + if self.filter: + idx = self.manifest_filter(self.filter) + else: + idx = self._data.index + self.sample_size = len(idx) + return None diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 15972d219..9dc298e69 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3186,7 +3186,7 @@ def add_filter_var(self, name, logic, overwrite=False): overwrite: bool, default False Overwrite an already existing filter-variable. """ - name = name.encode('utf8') + name = name.encode('utf8').replace(' ', '_').replace('~', '_') if name in self: if overwrite and not self.is_filter(name): msg = "Cannot add filter-variable '{}', a non-filter" @@ -3231,13 +3231,15 @@ def extend_filter_var(self, name, logic, extend_as=None): """ if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) + name = name.encode('utf8').replace(' ', '_').replace('~', '_') if extend_as: + extend_as = extend_as.encode('utf8').replace(' ', '_').replace('~', '_') f_name = '{}_{}'.format(name, extend_as) if f_name in self: msg = "Please change 'extend_as': '{}' is already in dataset." raise KeyError(msg.format(f_name)) self.copy(name, extend_as) - self._set_property(f_name, 'recoded_filter', True) + self._meta['columns'][f_name]['properties']['recoded_filter'] = True else: f_name = name self.uncode(f_name, {0: {f_name: 0}}) @@ -3301,6 +3303,8 @@ def manifest_filter(self, name): """ if not name: return self._data.index + else: + name = name.encode('utf8').replace(' ', '_').replace('~', '_') if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) return self.take({name: 0}) diff --git a/tests/test_batch.py b/tests/test_batch.py index 5c3aef76c..3174d27f1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -1,332 +1,332 @@ -import unittest -import os.path -import numpy as np -import pandas as pd -import quantipy as qp - -from quantipy.core.tools.view.logic import ( - has_any, has_all, has_count, - not_any, not_all, not_count, - is_lt, is_ne, is_gt, - is_le, is_eq, is_ge, - union, intersection) - -from quantipy.core.tools.dp.prep import frange -from collections import OrderedDict - -def _get_dataset(): - path = os.path.dirname(os.path.abspath(__file__)) + '/' - name = 'Example Data (A)' - casedata = '{}.csv'.format(name) - metadata = '{}.json'.format(name) - dataset = qp.DataSet(name, False) - dataset.set_verbose_infomsg(False) - dataset.set_verbose_errmsg(False) - dataset.read_quantipy(path+metadata, path+casedata) - return dataset - -def _get_batch(name, dataset=None, full=False): - if not dataset: dataset = _get_dataset() - batch = qp.Batch(dataset, name) - if full: - if not 'men only' in dataset: - dataset.add_filter_var('men only', {'gender': 1}) - batch.add_downbreak(['q1', 'q2', 'q6', 'age']) - batch.add_crossbreak(['gender', 'q2']) - batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') - batch.add_filter('men only') - batch.set_weights('weight_a') - return batch, dataset - -def _get_meta(batch): - name = batch.name - return batch._meta['sets']['batches'][name] - -class TestBatch(unittest.TestCase): - - def test_dataset_add_batch(self): - dataset = _get_dataset() - batch1 = dataset.add_batch('batch1') - batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) - self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 30) - b_meta = _get_meta(batch2) - self.assertEqual(b_meta['name'], 'batch2') - self.assertEqual(b_meta['cell_items'], ['c']) - self.assertEqual(b_meta['weights'], ['weight']) - self.assertEqual(b_meta['sigproperties']['siglevels'], [0.05]) - - def test_dataset_get_batch(self): - batch, ds = _get_batch('test', full=True) - self.assertRaises(KeyError, ds.get_batch, 'name') - b = ds.get_batch('test') - attr = ['xks', 'yks', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'name', 'total'] - for a in attr: - self.assertEqual(batch.__dict__[a], b.__dict__[a]) - - def test_from_batch(self): - ds = _get_dataset() - ds.force_texts('de-DE', 'en-GB') - batch1, ds = _get_batch('test1', ds, full=True) - batch1.set_language('de-DE') - batch1.hiding('q1', frange('8,9,96-99')) - batch1.slicing('q1', frange('9-4')) - batch2, ds = _get_batch('test2', ds) - batch2.add_downbreak('q1') - batch2.add_crossbreak('Wave') - batch2.as_addition('test1') - n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') - self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) - self.assertEqual(n_ds.variables(), [u'age', u'gender', u'q1', u'q2', - u'q6', u'q8a', u'q9a', u'Wave', - u'weight_a', u'RecordNo']) - self.assertEqual(n_ds['gender'].value_counts().values.tolist(), [3952]) - self.assertEqual(n_ds.value_texts('gender', 'en-GB'), [None, None]) - self.assertEqual(n_ds.value_texts('gender', 'de-DE'), [u'Male', u'Female']) - self.assertRaises(ValueError, ds.from_batch, 'test1', 'RecordNo', 'fr-FR') - - # ########################## methods used in _get_batch #################### - - def test_add_downbreak(self): - batch, ds = _get_batch('test') - batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', - u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', - u'q5_6', 'q14_1', u'q14r01c01', u'q14r02c01', - u'q14r03c01', u'q14r04c01', u'q14r05c01', u'q14r06c01', - u'q14r07c01', u'q14r08c01', u'q14r09c01', u'q14r10c01']) - self.assertEqual(b_meta['forced_names'], {'q3': 'q3_label', 'q5': 'q5_label'}) - self.assertEqual(b_meta['summaries'], ['q5', 'q14_1']) - x_y_map = [('q1', ['@']), ('q2', ['@']), ('q2b', ['@']), - ('q3', ['@']), ('q4', ['@']), ('q5', ['@']), - (u'q5_1', ['@']), (u'q5_2', ['@']), - (u'q5_3', ['@']), (u'q5_4', ['@']), - (u'q5_5', ['@']), (u'q5_6', ['@']), - ('q14_1', ['@']), (u'q14r01c01', ['@']), - (u'q14r02c01', ['@']), (u'q14r03c01', ['@']), - (u'q14r04c01', ['@']), (u'q14r05c01', ['@']), - (u'q14r06c01', ['@']), (u'q14r07c01', ['@']), - (u'q14r08c01', ['@']), (u'q14r09c01', ['@']), - (u'q14r10c01', ['@'])] - self.assertEqual(b_meta['x_y_map'], x_y_map) - - def test_add_crossbreak(self): - batch, ds = _get_batch('test') - batch.add_crossbreak(['gender', 'q2b']) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) - self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) - batch.add_downbreak('q1') - x_y_map = [('q1', ['@', 'gender', 'q2b'])] - self.assertEqual(b_meta['x_y_map'], x_y_map) - - def test_add_open_ends(self): - batch, ds = _get_batch('test') - self.assertRaises(ValueError, batch.add_open_ends, ['q8a', 'q9a'], None, - True, False, True, 'open ends', None) - batch.add_filter('men only', {'gender': 1}) - batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', filter_by={'age': is_ge(49)}) - verbatims = _get_meta(batch)['verbatims'][0] - self.assertEqual(verbatims['filter'], 'men only_open ends') - self.assertEqual(verbatims['columns'], ['q8a', 'q9a']) - self.assertEqual(verbatims['break_by'], ['RecordNo']) - self.assertEqual(verbatims['title'], 'open ends') - batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', split=True, - title=['open ends', 'open ends2'], overwrite=True) - verbatims = _get_meta(batch)['verbatims'] - self.assertEqual(len(verbatims), 2) - - def test_add_filter(self): - batch, ds = _get_batch('test', full=False) - batch.add_downbreak(['q1', 'q2b']) - batch.add_crossbreak('gender') - batch.add_filter('men only', {'gender': 1}) - b_meta = _get_meta(batch) - self.assertEqual(b_meta['filter'], 'men only') - x_filter_map = OrderedDict([('q1', 'men only'), - ('q2b', 'men only')]) - self.assertEqual(b_meta['x_filter_map'], x_filter_map) - self.assertEqual(b_meta['filter_names'], ['men only']) - - def test_set_weight(self): - batch, ds = _get_batch('test') - self.assertRaises(ValueError, batch.set_weights, 'Weight') - batch.set_weights('weight_a') - self.assertEqual(_get_meta(batch)['weights'], ['weight_a']) - self.assertEqual(batch.weights, ['weight_a']) - - # ########################################################################## - - def test_copy(self): - batch1, ds = _get_batch('test', full=True) - batch2 = batch1.clone('test_copy') - batch3 = batch1.clone('test_copy2', as_addition=True) - attributes = ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', - 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', - 'transposed_arrays', 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language'] - for a in attributes: - value = batch1.__dict__[a] - value2 = batch2.__dict__[a] - self.assertEqual(value, value2) - self.assertEqual(batch3.verbatims, []) - self.assertEqual(batch3.additional, True) - self.assertEqual(_get_meta(batch2)['name'], 'test_copy') - - def test_as_addition(self): - batch1, ds = _get_batch('test1', full=True) - batch2, ds = _get_batch('test2', ds, True) - batch2.as_addition('test1') - self.assertEqual(_get_meta(batch1)['additions'], ['test2']) - b_meta = _get_meta(batch2) - self.assertEqual(b_meta['additional'], True) - self.assertEqual(b_meta['verbatims'], []) - self.assertEqual(b_meta['y_on_y'], []) - - def test_set_cell_items(self): - batch, ds = _get_batch('test', full=True) - self.assertRaises(ValueError, batch.set_cell_items, ['c', 'pc']) - batch.set_cell_items('c') - self.assertEqual(_get_meta(batch)['cell_items'], ['c']) - self.assertEqual(batch.cell_items, ['c']) - - def test_set_language(self): - batch, ds = _get_batch('test', full=True) - self.assertRaises(ValueError, batch.set_language, 'en-gb') - batch.set_language('sv-SE') - self.assertEqual(_get_meta(batch)['language'], 'sv-SE') - self.assertEqual(batch.language, 'sv-SE') - - def test_set_sigtest(self): - batch, ds = _get_batch('test', full=True) - self.assertRaises(TypeError, batch.set_sigtests, [0.05, '0.01']) - batch.set_sigtests(.05) - self.assertEqual(_get_meta(batch)['sigproperties']['siglevels'], [0.05]) - - def test_make_summaries_transpose_arrays(self): - batch, ds = _get_batch('test') - b_meta = _get_meta(batch) - batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) - batch.make_summaries(None) - self.assertEqual(b_meta['summaries'], []) - batch.transpose_arrays(['q5', 'q6'], False) - batch.transpose_arrays(['q14_2', 'q14_3'], True) - self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) - t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} - self.assertEqual(b_meta['transposed_arrays'], t_a) - batch.make_summaries('q5') - self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) - self.assertRaises(ValueError, batch.make_summaries, 'q7') - - def test_extend_y(self): - batch1, ds = _get_batch('test1', full=True) - batch2, ds = _get_batch('test2', ds, True) - b_meta1 = _get_meta(batch1) - b_meta2 = _get_meta(batch2) - self.assertRaises(ValueError, batch1.extend_y, 'q2b', 'q5') - batch1.extend_y('q2b') - x_y_map = [('q1', ['@', 'gender', 'q2', 'q2b']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q2b']), - (u'q6_2', ['@', 'gender', 'q2', 'q2b']), - (u'q6_3', ['@', 'gender', 'q2', 'q2b']), - ('age', ['@', 'gender', 'q2', 'q2b'])] - self.assertEqual(b_meta1['x_y_map'], x_y_map) - self.assertEqual(b_meta1['extended_yks_global'], ['q2b']) - batch2.extend_y('q2b', 'q2') - batch2.extend_y('q3', 'q6') - extended_yks_per_x = {u'q6_3': ['q3'], 'q2': ['q2b'], u'q6_1': ['q3'], - u'q6_2': ['q3'], 'q6': ['q3']} - self.assertEqual(b_meta2['extended_yks_per_x'], extended_yks_per_x) - x_y_map = [('q1', ['@', 'gender', 'q2']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q3']), - (u'q6_2', ['@', 'gender', 'q2', 'q3']), - (u'q6_3', ['@', 'gender', 'q2', 'q3']), - ('age', ['@', 'gender', 'q2'])] - self.assertEqual(b_meta2['x_y_map'], x_y_map) - - def test_replace_y(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - self.assertRaises(ValueError, batch.replace_y, 'q2b', 'q5') - batch.replace_y('q2b', 'q6') - exclusive_yks_per_x = {u'q6_3': ['@', 'q2b'], - u'q6_1': ['@', 'q2b'], - u'q6_2': ['@', 'q2b'], - 'q6': ['@', 'q2b']} - self.assertEqual(b_meta['exclusive_yks_per_x'], exclusive_yks_per_x) - x_y_map = [('q1', ['@', 'gender', 'q2']), - ('q2', ['@', 'gender', 'q2']), - ('q6', ['@']), - (u'q6_1', ['@', 'q2b']), - (u'q6_2', ['@', 'q2b']), - (u'q6_3', ['@', 'q2b']), - ('age', ['@', 'gender', 'q2'])] - self.assertEqual(b_meta['x_y_map'], x_y_map) - - def test_extend_filter(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - ext_filters = {'q1': {'age': frange('20-25')}, ('q2', 'q6'): {'age': frange('30-35')}} - batch.extend_filter(ext_filters) - filter_names = ['men only', 'men only_q1', 'men only_q2', 'men only_q6'] - self.assertEqual(b_meta['filter_names'], filter_names) - x_filter_map = OrderedDict( - [('q1', 'men only_q1'), - ('q2', 'men only_q2'), - ('q6', 'men only_q6'), - ('q6_1', 'men only_q6'), - ('q6_2', 'men only_q6'), - ('q6_3', 'men only_q6'), - ('age', 'men only')]) - self.assertEqual(b_meta['x_filter_map'], x_filter_map) - - def test_add_y_on_y(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - batch.add_y_on_y('cross', {'age': frange('20-30')}, 'extend') - batch.add_y_on_y('back', None, 'replace') - self.assertEqual(b_meta['y_filter_map']['back'], None) - self.assertEqual(b_meta['y_on_y'], ['cross', 'back']) - - - ######################### meta edit methods ############################## - - def test_hiding(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - batch.hiding(['q1', 'q6'], [1, 2], ['x', 'y']) - for v in ['q1', u'q6_1', u'q6_2', u'q6_3']: - self.assertTrue(not b_meta['meta_edits'][v]['rules']['x'] == {}) - for v in ['q1', 'q6']: - self.assertTrue(not b_meta['meta_edits'][v]['rules']['y'] == {}) - - def test_sorting(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - batch.sorting(['q1', 'q6']) - for v in ['q1', u'q6_1', u'q6_2', u'q6_3']: - self.assertTrue(not b_meta['meta_edits'][v]['rules']['x'] == {}) - self.assertTrue(b_meta['meta_edits']['q6'].get('rules') is None) - - def test_slicing(self): - batch, ds = _get_batch('test', full=True) - b_meta = _get_meta(batch) - self.assertRaises(KeyError, batch.slicing, 'q6', [1, 2]) - batch.slicing(['q1', 'q2'], [3, 2, 1], ['x', 'y']) - for v in ['q1', 'q2']: - for ax in ['x', 'y']: - self.assertTrue(not b_meta['meta_edits'][v]['rules'][ax] == {}) - +import unittest +import os.path +import numpy as np +import pandas as pd +import quantipy as qp + +from quantipy.core.tools.view.logic import ( + has_any, has_all, has_count, + not_any, not_all, not_count, + is_lt, is_ne, is_gt, + is_le, is_eq, is_ge, + union, intersection) + +from quantipy.core.tools.dp.prep import frange +from collections import OrderedDict + +def _get_dataset(): + path = os.path.dirname(os.path.abspath(__file__)) + '/' + name = 'Example Data (A)' + casedata = '{}.csv'.format(name) + metadata = '{}.json'.format(name) + dataset = qp.DataSet(name, False) + dataset.set_verbose_infomsg(False) + dataset.set_verbose_errmsg(False) + dataset.read_quantipy(path+metadata, path+casedata) + return dataset + +def _get_batch(name, dataset=None, full=False): + if not dataset: dataset = _get_dataset() + batch = qp.Batch(dataset, name) + if full: + if not 'men_only' in dataset: + dataset.add_filter_var('men_only', {'gender': 1}) + batch.add_downbreak(['q1', 'q2', 'q6', 'age']) + batch.add_crossbreak(['gender', 'q2']) + batch.add_open_ends(['q8a', 'q9a'], 'RecordNo') + batch.add_filter('men_only') + batch.set_weights('weight_a') + return batch, dataset + +def _get_meta(batch): + name = batch.name + return batch._meta['sets']['batches'][name] + +class TestBatch(unittest.TestCase): + + def test_dataset_add_batch(self): + dataset = _get_dataset() + batch1 = dataset.add_batch('batch1') + batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) + self.assertTrue(isinstance(batch1, qp.Batch)) + self.assertEqual(len(_get_meta(batch1).keys()), 30) + b_meta = _get_meta(batch2) + self.assertEqual(b_meta['name'], 'batch2') + self.assertEqual(b_meta['cell_items'], ['c']) + self.assertEqual(b_meta['weights'], ['weight']) + self.assertEqual(b_meta['sigproperties']['siglevels'], [0.05]) + + def test_dataset_get_batch(self): + batch, ds = _get_batch('test', full=True) + self.assertRaises(KeyError, ds.get_batch, 'name') + b = ds.get_batch('test') + attr = ['xks', 'yks', 'filter', 'filter_names', + 'x_y_map', 'x_filter_map', 'y_on_y', + 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', + 'extended_yks_global', 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language', 'name', 'total'] + for a in attr: + self.assertEqual(batch.__dict__[a], b.__dict__[a]) + + def test_from_batch(self): + ds = _get_dataset() + ds.force_texts('de-DE', 'en-GB') + batch1, ds = _get_batch('test1', ds, full=True) + batch1.set_language('de-DE') + batch1.hiding('q1', frange('8,9,96-99')) + batch1.slicing('q1', frange('9-4')) + batch2, ds = _get_batch('test2', ds) + batch2.add_downbreak('q1') + batch2.add_crossbreak('Wave') + batch2.as_addition('test1') + n_ds = ds.from_batch('test1', 'RecordNo', 'de-DE', True, 'variables') + self.assertEqual(n_ds.codes('q1'), [7, 6, 5, 4]) + self.assertEqual(n_ds.variables(), [u'age', u'gender', u'q1', u'q2', + u'q6', u'q8a', u'q9a', u'Wave', + u'weight_a', u'RecordNo']) + self.assertEqual(n_ds['gender'].value_counts().values.tolist(), [3952]) + self.assertEqual(n_ds.value_texts('gender', 'en-GB'), [None, None]) + self.assertEqual(n_ds.value_texts('gender', 'de-DE'), [u'Male', u'Female']) + self.assertRaises(ValueError, ds.from_batch, 'test1', 'RecordNo', 'fr-FR') + + # ########################## methods used in _get_batch #################### + + def test_add_downbreak(self): + batch, ds = _get_batch('test') + batch.add_downbreak(['q1', 'q2', 'q2b', {'q3': 'q3_label'}, 'q4', {'q5': 'q5_label'}, 'q14_1']) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['xks'], ['q1', 'q2', 'q2b', 'q3', 'q4', 'q5', + u'q5_1', u'q5_2', u'q5_3', u'q5_4', u'q5_5', + u'q5_6', 'q14_1', u'q14r01c01', u'q14r02c01', + u'q14r03c01', u'q14r04c01', u'q14r05c01', u'q14r06c01', + u'q14r07c01', u'q14r08c01', u'q14r09c01', u'q14r10c01']) + self.assertEqual(b_meta['forced_names'], {'q3': 'q3_label', 'q5': 'q5_label'}) + self.assertEqual(b_meta['summaries'], ['q5', 'q14_1']) + x_y_map = [('q1', ['@']), ('q2', ['@']), ('q2b', ['@']), + ('q3', ['@']), ('q4', ['@']), ('q5', ['@']), + (u'q5_1', ['@']), (u'q5_2', ['@']), + (u'q5_3', ['@']), (u'q5_4', ['@']), + (u'q5_5', ['@']), (u'q5_6', ['@']), + ('q14_1', ['@']), (u'q14r01c01', ['@']), + (u'q14r02c01', ['@']), (u'q14r03c01', ['@']), + (u'q14r04c01', ['@']), (u'q14r05c01', ['@']), + (u'q14r06c01', ['@']), (u'q14r07c01', ['@']), + (u'q14r08c01', ['@']), (u'q14r09c01', ['@']), + (u'q14r10c01', ['@'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) + + def test_add_crossbreak(self): + batch, ds = _get_batch('test') + batch.add_crossbreak(['gender', 'q2b']) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['yks'], ['@', 'gender', 'q2b']) + self.assertRaises(KeyError, batch.add_crossbreak, ['@', 'GENDER']) + batch.add_downbreak('q1') + x_y_map = [('q1', ['@', 'gender', 'q2b'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) + + def test_add_open_ends(self): + batch, ds = _get_batch('test') + self.assertRaises(ValueError, batch.add_open_ends, ['q8a', 'q9a'], None, + True, False, True, 'open ends', None) + batch.add_filter('men_only', {'gender': 1}) + batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', filter_by={'age': is_ge(49)}) + verbatims = _get_meta(batch)['verbatims'][0] + self.assertEqual(verbatims['filter'], 'men_only_open ends') + self.assertEqual(verbatims['columns'], ['q8a', 'q9a']) + self.assertEqual(verbatims['break_by'], ['RecordNo']) + self.assertEqual(verbatims['title'], 'open ends') + batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', split=True, + title=['open ends', 'open ends2'], overwrite=True) + verbatims = _get_meta(batch)['verbatims'] + self.assertEqual(len(verbatims), 2) + + def test_add_filter(self): + batch, ds = _get_batch('test', full=False) + batch.add_downbreak(['q1', 'q2b']) + batch.add_crossbreak('gender') + batch.add_filter('men_only', {'gender': 1}) + b_meta = _get_meta(batch) + self.assertEqual(b_meta['filter'], 'men_only') + x_filter_map = OrderedDict([('q1', 'men_only'), + ('q2b', 'men_only')]) + self.assertEqual(b_meta['x_filter_map'], x_filter_map) + self.assertEqual(b_meta['filter_names'], ['men_only']) + + def test_set_weight(self): + batch, ds = _get_batch('test') + self.assertRaises(ValueError, batch.set_weights, 'Weight') + batch.set_weights('weight_a') + self.assertEqual(_get_meta(batch)['weights'], ['weight_a']) + self.assertEqual(batch.weights, ['weight_a']) + + # ########################################################################## + + def test_copy(self): + batch1, ds = _get_batch('test', full=True) + batch2 = batch1.clone('test_copy') + batch3 = batch1.clone('test_copy2', as_addition=True) + attributes = ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', + 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', + 'transposed_arrays', 'extended_yks_global', 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language'] + for a in attributes: + value = batch1.__dict__[a] + value2 = batch2.__dict__[a] + self.assertEqual(value, value2) + self.assertEqual(batch3.verbatims, []) + self.assertEqual(batch3.additional, True) + self.assertEqual(_get_meta(batch2)['name'], 'test_copy') + + def test_as_addition(self): + batch1, ds = _get_batch('test1', full=True) + batch2, ds = _get_batch('test2', ds, True) + batch2.as_addition('test1') + self.assertEqual(_get_meta(batch1)['additions'], ['test2']) + b_meta = _get_meta(batch2) + self.assertEqual(b_meta['additional'], True) + self.assertEqual(b_meta['verbatims'], []) + self.assertEqual(b_meta['y_on_y'], []) + + def test_set_cell_items(self): + batch, ds = _get_batch('test', full=True) + self.assertRaises(ValueError, batch.set_cell_items, ['c', 'pc']) + batch.set_cell_items('c') + self.assertEqual(_get_meta(batch)['cell_items'], ['c']) + self.assertEqual(batch.cell_items, ['c']) + + def test_set_language(self): + batch, ds = _get_batch('test', full=True) + self.assertRaises(ValueError, batch.set_language, 'en-gb') + batch.set_language('sv-SE') + self.assertEqual(_get_meta(batch)['language'], 'sv-SE') + self.assertEqual(batch.language, 'sv-SE') + + def test_set_sigtest(self): + batch, ds = _get_batch('test', full=True) + self.assertRaises(TypeError, batch.set_sigtests, [0.05, '0.01']) + batch.set_sigtests(.05) + self.assertEqual(_get_meta(batch)['sigproperties']['siglevels'], [0.05]) + + def test_make_summaries_transpose_arrays(self): + batch, ds = _get_batch('test') + b_meta = _get_meta(batch) + batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + batch.make_summaries(None) + self.assertEqual(b_meta['summaries'], []) + batch.transpose_arrays(['q5', 'q6'], False) + batch.transpose_arrays(['q14_2', 'q14_3'], True) + self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) + t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} + self.assertEqual(b_meta['transposed_arrays'], t_a) + batch.make_summaries('q5') + self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) + self.assertRaises(ValueError, batch.make_summaries, 'q7') + + def test_extend_y(self): + batch1, ds = _get_batch('test1', full=True) + batch2, ds = _get_batch('test2', ds, True) + b_meta1 = _get_meta(batch1) + b_meta2 = _get_meta(batch2) + self.assertRaises(ValueError, batch1.extend_y, 'q2b', 'q5') + batch1.extend_y('q2b') + x_y_map = [('q1', ['@', 'gender', 'q2', 'q2b']), + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q2b']), + (u'q6_2', ['@', 'gender', 'q2', 'q2b']), + (u'q6_3', ['@', 'gender', 'q2', 'q2b']), + ('age', ['@', 'gender', 'q2', 'q2b'])] + self.assertEqual(b_meta1['x_y_map'], x_y_map) + self.assertEqual(b_meta1['extended_yks_global'], ['q2b']) + batch2.extend_y('q2b', 'q2') + batch2.extend_y('q3', 'q6') + extended_yks_per_x = {u'q6_3': ['q3'], 'q2': ['q2b'], u'q6_1': ['q3'], + u'q6_2': ['q3'], 'q6': ['q3']} + self.assertEqual(b_meta2['extended_yks_per_x'], extended_yks_per_x) + x_y_map = [('q1', ['@', 'gender', 'q2']), + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q3']), + (u'q6_2', ['@', 'gender', 'q2', 'q3']), + (u'q6_3', ['@', 'gender', 'q2', 'q3']), + ('age', ['@', 'gender', 'q2'])] + self.assertEqual(b_meta2['x_y_map'], x_y_map) + + def test_replace_y(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + self.assertRaises(ValueError, batch.replace_y, 'q2b', 'q5') + batch.replace_y('q2b', 'q6') + exclusive_yks_per_x = {u'q6_3': ['@', 'q2b'], + u'q6_1': ['@', 'q2b'], + u'q6_2': ['@', 'q2b'], + 'q6': ['@', 'q2b']} + self.assertEqual(b_meta['exclusive_yks_per_x'], exclusive_yks_per_x) + x_y_map = [('q1', ['@', 'gender', 'q2']), + ('q2', ['@', 'gender', 'q2']), + ('q6', ['@']), + (u'q6_1', ['@', 'q2b']), + (u'q6_2', ['@', 'q2b']), + (u'q6_3', ['@', 'q2b']), + ('age', ['@', 'gender', 'q2'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) + + def test_extend_filter(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + ext_filters = {'q1': {'age': frange('20-25')}, ('q2', 'q6'): {'age': frange('30-35')}} + batch.extend_filter(ext_filters) + filter_names = ['men_only', 'men_only_q1', 'men_only_q2', 'men_only_q6'] + self.assertEqual(b_meta['filter_names'], filter_names) + x_filter_map = OrderedDict( + [('q1', 'men_only_q1'), + ('q2', 'men_only_q2'), + ('q6', 'men_only_q6'), + ('q6_1', 'men_only_q6'), + ('q6_2', 'men_only_q6'), + ('q6_3', 'men_only_q6'), + ('age', 'men_only')]) + self.assertEqual(b_meta['x_filter_map'], x_filter_map) + + def test_add_y_on_y(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + batch.add_y_on_y('cross', {'age': frange('20-30')}, 'extend') + batch.add_y_on_y('back', None, 'replace') + self.assertEqual(b_meta['y_filter_map']['back'], None) + self.assertEqual(b_meta['y_on_y'], ['cross', 'back']) + + + ######################### meta edit methods ############################## + + def test_hiding(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + batch.hiding(['q1', 'q6'], [1, 2], ['x', 'y']) + for v in ['q1', u'q6_1', u'q6_2', u'q6_3']: + self.assertTrue(not b_meta['meta_edits'][v]['rules']['x'] == {}) + for v in ['q1', 'q6']: + self.assertTrue(not b_meta['meta_edits'][v]['rules']['y'] == {}) + + def test_sorting(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + batch.sorting(['q1', 'q6']) + for v in ['q1', u'q6_1', u'q6_2', u'q6_3']: + self.assertTrue(not b_meta['meta_edits'][v]['rules']['x'] == {}) + self.assertTrue(b_meta['meta_edits']['q6'].get('rules') is None) + + def test_slicing(self): + batch, ds = _get_batch('test', full=True) + b_meta = _get_meta(batch) + self.assertRaises(KeyError, batch.slicing, 'q6', [1, 2]) + batch.slicing(['q1', 'q2'], [3, 2, 1], ['x', 'y']) + for v in ['q1', 'q2']: + for ax in ['x', 'y']: + self.assertTrue(not b_meta['meta_edits'][v]['rules'][ax] == {}) + From 2d94c60809ec829786a35538d3c6444166e655c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 30 Oct 2018 11:34:00 +0100 Subject: [PATCH 577/733] filter for open ends and for y_on_y --- quantipy/core/batch.py | 96 ++++++++++++++++++++++++---------------- quantipy/core/dataset.py | 14 ++++++ tests/test_batch.py | 4 +- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 59a7cfa0e..0dfabc04e 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -139,6 +139,7 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.x_y_map = None self.x_filter_map = None self.y_on_y = [] + self.y_on_y_filter = {} self.y_filter_map = {} self.forced_names = {} self.summaries = [] @@ -177,16 +178,17 @@ def _update(self): """ self._map_x_to_y() self._map_x_to_filter() + self._map_y_on_y_filter() self._samplesize_from_batch_filter() attrs = self.__dict__ for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', + 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', - 'unwgt_counts', 'y_filter_map', 'build_info', + 'unwgt_counts', 'y_filter_map', 'build_info' ]: attr_update = {attr: attrs.get(attr, attrs.get('_{}'.format(attr)))} self._meta['sets']['batches'][self.name].update(attr_update) @@ -197,7 +199,7 @@ def _load_batch(self): """ bdefs = self._meta['sets']['batches'][self.name] for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', + 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', @@ -411,7 +413,7 @@ def as_addition(self, batch_name): self.additional = True self.verbatims = [] self.y_on_y = [] - self.y_filter_map = {} + self.y_on_y_filter = {} if self._verbose_infos: msg = ("Batch '{}' specified as addition to Batch '{}'. Any open end " "summaries and 'y_on_y' agg. have been removed!") @@ -763,6 +765,7 @@ def remove_filter(self): self.filter = None self.filter_names = [] self.extended_filters_per_x = {} + self.y_on_y_filter = {} self._update() return None @@ -813,21 +816,14 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): if filter_by: + f_name = title if not self.filter else '%s_%s' % (self.filter, title) + f_name = self._verify_filter_name(f_name) + logic = {'label': title, 'logic': filter_by} if self.filter: - f_name = '{}_{}'.format(self.filter, title) - else: - f_name = '{}_f'.format(title) - if self.is_filter(f_name): - logic = intersection([{self.filter: 0}, filter_by]) - if not self.take(logic).index.tolist() == self.manifest_filter(f_name): - msg = "'{}' is already in use with an other logic." - raise ValueError(msg.format(f_name)) + suffix = f_name[len(self.filter)+1:] + self.extend_filter_var(self.filter, logic, suffix) else: - logic = {'label': title, 'logic': filter_by} - if self.filter: - self.extend_filter_var(self.filter, logic, title) - else: - self.add_filter_var(f_name, logic) + self.add_filter_var(f_name, logic) slicer = f_name else: slicer = self.filter @@ -971,15 +967,17 @@ def add_y_on_y(self, name, y_filter=None, main_filter='extend'): ---------- name: str key name for the y on y aggregation. - y_filter: dict (complex logic), default None + y_filter: str (filter var name) or dict (complex logic), default None Add a filter for the y on y aggregation. If None is provided - the main batch filter is taken. + the main batch filter is taken ('extend') or no filter logic is + applied ('replace'). main_filter: {'extend', 'replace'}, default 'extend' Defines if the main batch filter is extended or replaced by the y_on_y filter. - In order to remove all filters from the y on y aggregation set - ``y_filter='no_filter'`` and ``main_filter='replace'``. + Note: + If the y_filter is provided as a str (filter var name), + main_filter is automatically set to 'replace'. Returns ------- @@ -991,25 +989,12 @@ def add_y_on_y(self, name, y_filter=None, main_filter='extend'): raise ValueError("'main_filter' must be either 'extend' or 'replace'.") if not name in self.y_on_y: self.y_on_y.append(name) - if y_filter is not None: - logic = {'label': name, 'logic': y_filter} - if main_filter == 'extend': - if self.filter: - f_name = '{}_{}'.format(self.filter, name) - self.extend_filter_var(self.filter, logic, name) - else: - f_name = '{}_f'.format(name) - self.add_filter_var(f_name, logic) - elif main_filter == 'replace': - f_name = '{}_f'.format(name) - self.add_filter_var(f_name, logic) - else: - if main_filter == 'replace': - f_name = None + if isinstance(y_filter, basestring): + if not self.is_filter(y_filter): + raise ValueError('{} is not a valid filter var.'.format(y_filter)) else: - f_name = self.filter - - self.y_filter_map[name] = f_name + main_filter = 'replace' + self.y_on_y_filter[name] = (main_filter, y_filter) self._update() return None @@ -1090,6 +1075,39 @@ def _map_x_to_filter(self): self.x_filter_map = mapping return None + def _map_y_on_y_filter(self): + """ + Get all y_on_y filters and map them with the main filter. + Returns + ------- + None + """ + self.y_filter_map = {} + for y_on_y in self.y_on_y: + ext_rep, y_f = self.y_on_y_filter[y_on_y] + logic = {'label': y_on_y, 'logic': y_f} + if ext_rep == 'replace': + if not y_f: + f = None + elif isinstance(y_f, basestring): + f = y_f + else: + f = self._verify_filter_name(y_on_y) + self.add_filter_var(f, logic) + elif ext_rep == 'extend': + if not y_f: + f = self.filter + elif not self.filter: + f = self._verify_filter_name(y_on_y) + self.add_filter_var(f, logic) + else: + f = '{}_{}'.format(self.filter, y_on_y) + f = self._verify_filter_name(f) + suf = f[len(self.filter)+1:] + self.extend_filter_var(self.filter, logic, suf) + self.y_filter_map[y_on_y] = f + return None + def _check_forced_names(self, variables): """ Store forced names for xks and return adjusted list of downbreaks. diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9dc298e69..ab04cb6f1 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2217,6 +2217,14 @@ def _get_meta(self, var, type=None, text_key=None, axis_edit=None): def _pad_meta_list(meta_list, pad_to_len): return meta_list + ([''] * pad_to_len) + def enumerator(self, name): + x = 1 + n = name + while n in self: + x += 1 + n = '{}_{}'.format(name, x) + return n + # ------------------------------------------------------------------------ # fix/ repair meta data # ------------------------------------------------------------------------ @@ -3276,6 +3284,12 @@ def _transform_filter_logics(self, logic, start): values.append(val) return values + def _verify_filter_name(self, name): + f = '{}_f'.format(name) + f = f.encode('utf8').replace(' ', '_').replace('~', '_') + f = self.enumerator(f) + return f + @modify(to_list=['values']) def reduce_filter_var(self, name, values): """ diff --git a/tests/test_batch.py b/tests/test_batch.py index 3174d27f1..caec5d33c 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -49,7 +49,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 30) + self.assertEqual(len(_get_meta(batch1).keys()), 31) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) @@ -134,7 +134,7 @@ def test_add_open_ends(self): batch.add_filter('men_only', {'gender': 1}) batch.add_open_ends(['q8a', 'q9a'], 'RecordNo', filter_by={'age': is_ge(49)}) verbatims = _get_meta(batch)['verbatims'][0] - self.assertEqual(verbatims['filter'], 'men_only_open ends') + self.assertEqual(verbatims['filter'], 'men_only_open_ends_f') self.assertEqual(verbatims['columns'], ['q8a', 'q9a']) self.assertEqual(verbatims['break_by'], ['RecordNo']) self.assertEqual(verbatims['title'], 'open ends') From 0b899d4eeeefb8aae6d3d837ec2c3d99399ec530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 30 Oct 2018 12:07:09 +0100 Subject: [PATCH 578/733] warning for stats on delimited set --- quantipy/core/stack.py | 5555 ++++++++++++++++++++-------------------- 1 file changed, 2783 insertions(+), 2772 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index a3bd9824f..45c3f2761 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -1,2772 +1,2783 @@ - -#-*- coding: utf-8 -*- -import io -import itertools -import json -import pandas as pd -import numpy as np -import quantipy as qp -import copy -import time -import sys -import warnings - -from link import Link -from chain import Chain -from view import View -from helpers import functions -from view_generators.view_mapper import ViewMapper -from view_generators.view_maps import QuantipyViews -from quantipy.core.tools.qp_decorators import modify -from quantipy.core.tools.dp.spss.reader import parse_sav_file -from quantipy.core.tools.dp.io import unicoder, write_quantipy -from quantipy.core.tools.dp.prep import frequency, verify_test_results, frange -from quantipy.core.tools.view.logic import ( - has_any, has_all, has_count, - not_any, not_all, not_count, - is_lt, is_ne, is_gt, - is_le, is_eq, is_ge, - union, intersection, get_logic_index) -from cache import Cache - -import itertools -from collections import defaultdict, OrderedDict - -# Pickle modules -import cPickle - -# Compression methods -import gzip - -from quantipy.sandbox.sandbox import Chain as NewChain -from quantipy.sandbox.sandbox import ChainManager - - -class Stack(defaultdict): - """ - Container of quantipy.Link objects holding View objects. - - A Stack is nested dictionary that structures the data and variable - relationships storing all View aggregations performed. - """ - - def __init__(self, - name="", - add_data=None): - - super(Stack, self).__init__(Stack) - - self.name = name - self.key = None - self.parent = None - - # This is the root of the stack - # It is used by the get/set methods to determine - # WHERE in the stack those methods are. - self.stack_pos = "stack_root" - - self.x_variables = None - self.y_variables = None - - self.__view_keys = [] - - if add_data: - for key in add_data: - if isinstance(add_data[key], dict): - self.add_data( - data_key=key, - data=add_data[key].get('data', None), - meta=add_data[key].get('meta', None) - ) - elif isinstance(add_data[key], tuple): - self.add_data( - data_key=key, - data=add_data[key][0], - meta=add_data[key][1] - ) - else: - raise TypeError( - "All data_key values must be one of the following types: " - " or . " - "Given: %s" % (type(add_data[key])) - ) - - def __setstate__(self, attr_dict): - self.__dict__.update(attr_dict) - - def __reduce__(self): - arguments = (self.name, ) - state = self.__dict__.copy() - if 'cache' in state: - state.pop('cache') - state['cache'] = Cache() # Empty the cache for storage - return self.__class__, arguments, state, None, self.iteritems() - - def __setitem__(self, key, val): - """ The 'set' method for the Stack(dict) - - It 'sets' the value in it's correct place in the Stack - AND applies a 'stack_pos' value depending on WHERE in - the stack the value is being placed. - """ - super(Stack, self).__setitem__(key, val) - - # The 'meta' portion of the stack is a standar dict (not Stack) - try: - if isinstance(val, Stack) and val.stack_pos is "stack_root": - val.parent = self - val.key = key - - # This needs to be compacted and simplified. - if self.stack_pos is "stack_root": - val.stack_pos = "data_root" - elif self.stack_pos is "data_root": - val.stack_pos = "filter" - elif self.stack_pos is "filter": - val.stack_pos = "x" - - except AttributeError: - pass - - def __getitem__(self, key): - """ The 'get' method for the Stack(dict) - - The method 'gets' a value from the stack. If 'stack_pos' is 'y' - AND the value isn't a Link instance THEN it tries to query the - stack again with the x/y variables swapped and IF that yelds - a result that is a Link object THEN it sets a 'transpose' variable - as True in the result and the result is transposed. - """ - val = defaultdict.__getitem__(self, key) - return val - - def add_data(self, data_key, data=None, meta=None, ): - """ - Sets the data_key into the stack, optionally mapping data sources it. - - It is possible to handle the mapping of data sources in different ways: - - * no meta or data (for proxy links not connected to source data) - * meta only (for proxy links with supporintg meta) - * data only (meta will be inferred if possible) - * data and meta - - Parameters - ---------- - data_key : str - The reference name for a data source connected to the Stack. - data : pandas.DataFrame - The input (case) data source. - meta : dict or OrderedDict - A quantipy compatible metadata source that describes the case data. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_key) - - if data_key in self.keys(): - warning_msg = "You have overwritten data/meta for key: ['%s']." - print warning_msg % (data_key) - - if data is not None: - if isinstance(data, pd.DataFrame): - if meta is None: - # To do: infer meta from DataFrame - meta = {'info': None, 'lib': None, 'sets': None, - 'columns': None, 'masks': None} - # Add a special column of 1s - data['@1'] = np.ones(len(data.index)) - data.index = list(xrange(0, len(data.index))) - else: - raise TypeError( - "The 'data' given to Stack.add_data() must be one of the following types: " - "" - ) - - if not meta is None: - if isinstance(meta, (dict, OrderedDict)): - # To do: verify incoming meta - pass - else: - raise TypeError( - "The 'meta' given to Stack.add_data() must be one of the following types: " - ", ." - ) - - # Add the data key to the stack - # self[data_key] = {} - - # Add the meta and data to the data_key position in the stack - self[data_key].meta = meta - self[data_key].data = data - self[data_key].cache = Cache() - self[data_key]['no_filter'].data = self[data_key].data - - def remove_data(self, data_keys): - """ - Deletes the data_key(s) and associated data specified in the Stack. - - Parameters - ---------- - data_keys : str or list of str - The data keys to remove. - - Returns - ------- - None - """ - self._verify_key_types(name='data', keys=data_keys) - if isinstance(data_keys, (str, unicode)): - data_keys = [data_keys] - for data_key in data_keys: - del self[data_key] - - def variable_types(self, data_key, only_type=None, verbose=True): - """ - Group variables by data types found in the meta. - - Parameters - ---------- - data_key : str - The reference name of a case data source hold by the Stack instance. - only_type : {'int', 'float', 'single', 'delimited set', 'string', - 'date', time', 'array'}, optional - Will restrict the output to the given data type. - - Returns - ------- - types : dict or list of str - A summary of variable names mapped to their data types, in form of - {type_name: [variable names]} or a list of variable names - confirming only_type. - """ - if self[data_key].meta['columns'] is None: - return 'No meta attached to data_key: %s' %(data_key) - else: - types = { - 'int': [], - 'float': [], - 'single': [], - 'delimited set': [], - 'string': [], - 'date': [], - 'time': [], - 'array': [] - } - not_found = [] - for col in self[data_key].data.columns: - if not col in ['@1', 'id_L1', 'id_L1.1']: - try: - types[ - self[data_key].meta['columns'][col]['type'] - ].append(col) - except: - not_found.append(col) - for mask in self[data_key].meta['masks'].keys(): - types[self[data_key].meta['masks'][mask]['type']].append(mask) - if not_found and verbose: - print '%s not found in meta file. Ignored.' %(not_found) - if only_type: - return types[only_type] - else: - return types - - def apply_meta_edits(self, batch_name, data_key, filter_key=None, - freeze=False): - """ - Take over meta_edits from Batch definitions. - - Parameters - ---------- - batch_name: str - Name of the Batch whose meta_edits are taken. - data_key: str - Accessing this metadata: ``self[data_key].meta`` - Batch definitions are takes from here and this metadata is modified. - filter_key: str, default None - Currently not implemented! - Accessing this metadata: ``self[data_key][filter_key].meta`` - Batch definitions are takes from here and this metadata is modified. - """ - if filter_key: - raise NotImplementedError("'filter_key' is not implemented.") - if freeze: - self.freeze_master_meta(data_key) - meta = self[data_key].meta - batch = meta['sets']['batches'][batch_name] - for name, e_meta in batch['meta_edits'].items(): - if name == 'lib': - continue - elif name in meta['masks']: - meta['masks'][name] = e_meta - try: - lib = batch['meta_edits']['lib'][name] - meta['lib']['values'][name] = lib - except: - pass - else: - meta['columns'][name] = e_meta - meta['lib']['default text'] = batch['language'] - return None - - def freeze_master_meta(self, data_key, filter_key=None): - """ - Save ``.meta`` in ``.master_meta`` for a defined data_key. - - Parameters - ---------- - data_key: str - Using: ``self[data_key]`` - filter_key: str, default None - Currently not implemented! - Using: ``self[data_key][filter_key]`` - """ - if filter_key: - raise NotImplementedError("'filter_key' is not implemented.") - self[data_key].master_meta = copy.deepcopy(self[data_key].meta) - self[data_key].meta = copy.deepcopy(self[data_key].meta) - return None - - def restore_meta(self, data_key, filter_key=None): - """ - Restore the ``.master_meta`` for a defined data_key if it exists. - - Undo self.apply_meta_edits() - - Parameters - ---------- - data_key: str - Accessing this metadata: ``self[data_key].meta`` - filter_key: str, default None - Currently not implemented! - Accessing this metadata: ``self[data_key][filter_key].meta`` - """ - if filter_key: - raise NotImplementedError("'filter_key' is not implemented.") - try: - self[data_key].meta = copy.deepcopy(self[data_key].master_meta) - except: - pass - return None - - def get_chain(self, *args, **kwargs): - - if qp.OPTIONS['new_chains']: - chain = ChainManager(self) - chain = chain.get(*args, **kwargs) - return chain - else: - def _get_chain(name=None, data_keys=None, filters=None, x=None, y=None, - views=None, orient_on=None, select=None, - rules=False, rules_weight=None, described=None): - """ - Construct a "chain" shaped subset of Links and their Views from the Stack. - - A chain is a one-to-one or one-to-many relation with an orientation that - defines from which axis (x or y) it is build. - - Parameters - ---------- - name : str, optional - If not provided the name of the chain is generated automatically. - data_keys, filters, x, y, views : str or list of str - Views will be added reflecting the order in ``views`` parameter. If - both ``x`` and ``y`` have multiple items, you must specify the - ``orient_on`` parameter. - orient_on : {'x', 'y'}, optional - Must be specified if both ``x`` and ``y`` are lists of multiple - items. - select : tbc. - :TODO: document this! - - Returns - ------- - chain : Chain object instance - """ - - #Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - # filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if orient_on is None: - if len(x)==1: - orientation = 'x' - elif len(y)==1: - orientation = 'y' - else: - orientation = 'x' - else: - orientation = orient_on - if described is None: - described = self.describe() - - if isinstance(rules, bool): - if rules: - rules = ['x', 'y'] - else: - rules = [] - - if orient_on: - if x is None: - x = described['x'].drop_duplicates().values.tolist() - if y is None: - y = described['y'].drop_duplicates().values.tolist() - if views is None: - views = self._Stack__view_keys - views = [v for v in views if '|default|' not in v] - chains = self.__get_chains( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y, - views=views, - orientation=orient_on, - select=select, - rules=rules, - rules_weight=rules_weight) - return chains - else: - chain = Chain(name) - found_views = [] - - #Make sure all the given keys are in lists - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - if data_keys is None: - # Apply lazy data_keys if none given - data_keys = self.keys() - - the_filter = "no_filter" if filters is None else filters - - if self.__has_list(data_keys): - for key in data_keys: - # Use describe method to get x keys if not supplied. - if x is None: - x_keys = described['x'].drop_duplicates().values.tolist() - else: - x_keys = x - - # Use describe method to get y keys if not supplied. - if y is None: - y_keys = described['y'].drop_duplicates().values.tolist() - else: - y_keys = y - - # Use describe method to get view keys if not supplied. - if views is None: - v_keys = described['view'].drop_duplicates().values.tolist() - v_keys = [v_key for v_key in v_keys if '|default|' - not in v_key] - else: - v_keys = views - - chain._derive_attributes( - key, the_filter, x_keys, y_keys, views, orientation=orientation) - - # Apply lazy name if none given - if name is None: - chain._lazy_name() - - for x_key in x_keys: - self._verify_key_exists( - x_key, - stack_path=[key, the_filter] - ) - - for y_key in y_keys: - self._verify_key_exists( - y_key, - stack_path=[key, the_filter, x_key]) - - - try: - base_text = self[key].meta['columns'][x_key]['properties']['base_text'] - if isinstance(base_text, (str, unicode)): - if base_text.startswith(('Base:', 'Bas:')): - base_text = base_text.split(':')[-1].lstrip() - elif isinstance(base_text, dict): - for text_key in base_text.keys(): - if base_text[text_key].startswith(('Base:', 'Bas:')): - base_text[text_key] = base_text[text_key].split(':')[-1].lstrip() - chain.base_text = base_text - except: - pass - if views is None: - chain[key][the_filter][x_key][y_key] = self[key][the_filter][x_key][y_key] - else: - stack_link = self[key][the_filter][x_key][y_key] - link_keys = stack_link.keys() - chain_link = {} - chain_view_keys = [k for k in views if k in link_keys] - for vk in chain_view_keys: - stack_view = stack_link[vk] - # Get view dataframe - rules_x_slicer = self.axis_slicer_from_vartype( - rules, 'x', key, the_filter, x_key, y_key, rules_weight) - - rules_y_slicer = self.axis_slicer_from_vartype( - rules, 'y', key, the_filter, x_key, y_key, rules_weight) - if rules_x_slicer is None and rules_y_slicer is None: - # No rules to apply - view_df = stack_view.dataframe - else: - # Apply rules - viable_axes = functions.rule_viable_axes(self[key].meta, vk, x_key, y_key) - transposed_array_sum = x_key == '@' and y_key in self[key].meta['masks'] - if not viable_axes: - # Axes are not viable for rules application - view_df = stack_view.dataframe - else: - view_df = stack_view.dataframe.copy() - if 'x' in viable_axes and not rules_x_slicer is None: - # Apply x-rules - rule_codes = set(rules_x_slicer) - view_codes = set(view_df.index.tolist()) - if not rule_codes - view_codes: - view_df = view_df.loc[rules_x_slicer] - if 'x' in viable_axes and transposed_array_sum and rules_y_slicer: - view_df = view_df.loc[rules_y_slicer] - if 'y' in viable_axes and not rules_y_slicer is None: - # Apply y-rules - view_df = view_df[rules_y_slicer] - if vk.split('|')[1].startswith('t.'): - view_df = verify_test_results(view_df) - chain_view = View( - link=stack_link, - name = stack_view.name, - kwargs=stack_view._kwargs) - chain_view._notation = vk - chain_view.grp_text_map = stack_view.grp_text_map - chain_view.dataframe = view_df - chain_view._custom_txt = stack_view._custom_txt - chain_view.add_base_text = stack_view.add_base_text - chain_link[vk] = chain_view - if vk not in found_views: - found_views.append(vk) - - chain[key][the_filter][x_key][y_key] = chain_link - else: - raise ValueError( - "One or more of your data_keys ({data_keys}) is not" - " in the stack ({stack_keys})".format( - data_keys=data_keys, - stack_keys=self.keys() - ) - ) - - # Make sure chain.views only contains views that actually exist - # in the chain - if found_views: - chain.views = [ - view - for view in chain.views - if view in found_views] - return chain - - return _get_chain(*args, **kwargs) - - def reduce(self, data_keys=None, filters=None, x=None, y=None, variables=None, views=None): - ''' - Remove keys from the matching levels, erasing discrete Stack portions. - - Parameters - ---------- - data_keys, filters, x, y, views : str or list of str - - Returns - ------- - None - ''' - - # Ensure given keys are all valid types - self._verify_multiple_key_types( - data_keys=data_keys, - filters=filters, - x=x, - y=y, - variables=variables, - views=views - ) - - # Make sure all the given keys are in lists - data_keys = self._force_key_as_list(data_keys) - filters = self._force_key_as_list(filters) - views = self._force_key_as_list(views) - if not variables is None: - variables = self._force_key_as_list(variables) - x = variables - y = variables - else: - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Make sure no keys that don't exist anywhere were passed - key_check = { - 'data': data_keys, - 'filter': filters, - 'x': x, - 'y': y, - 'view': views - } - - contents = self.describe() - for key_type, keys in key_check.iteritems(): - if not keys is None: - uk = contents[key_type].unique() - if not any([tk in uk for tk in keys]): - raise ValueError( - "Some of the %s keys passed to stack.reduce() " - "weren't found. Found: %s. " - "Given: %s" % (key_type, uk, keys) - ) - - if not data_keys is None: - for dk in data_keys: - try: - del self[dk] - except: - pass - - for dk in self.keys(): - if not filters is None: - for fk in filters: - try: - del self[dk][fk] - except: - pass - - for fk in self[dk].keys(): - if not x is None: - for xk in x: - try: - del self[dk][fk][xk] - except: - pass - - for xk in self[dk][fk].keys(): - if not y is None: - for yk in y: - try: - del self[dk][fk][xk][yk] - except: - pass - - for yk in self[dk][fk][xk].keys(): - if not views is None: - for vk in views: - try: - del self[dk][fk][xk][yk][vk] - except: - pass - - def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, - views=None, weights=None, variables=None): - """ - Add Link and View defintions to the Stack. - - The method can be used flexibly: It is possible to pass only Link - defintions that might be composed of filter, x and y specifications, - only views incl. weight variable selections or arbitrary combinations of - the former. - - :TODO: Remove ``variables`` from parameter list and method calls. - - Parameters - ---------- - data_keys : str, optional - The data_key to be added to. If none is given, the method will try - to add to all data_keys found in the Stack. - filters : list of str describing filter defintions, default ['no_filter'] - The string must be a valid input for the - pandas.DataFrame.query() method. - x, y : str or list of str - The x and y variables to constrcut Links from. - views : list of view method names. - Can be any of Quantipy's preset Views or the names of created - view method specifications. - weights : list, optional - The names of weight variables to consider in the data aggregation - process. Weight variables must be of type ``float``. - - Returns - ------- - None - """ - if data_keys is None: - data_keys = self.keys() - else: - self._verify_key_types(name='data', keys=data_keys) - data_keys = self._force_key_as_list(data_keys) - - if not isinstance(views, ViewMapper): - # Use DefaultViews if no view were given - if views is None: - pass - # views = DefaultViews() - elif isinstance(views, (list, tuple)): - views = QuantipyViews(views=views) - else: - raise TypeError( - "The views past to stack.add_link() must be type , " - "or they must be a list of method names known to ." - ) - - qplogic_filter = False - if not isinstance(filters, dict): - self._verify_key_types(name='filter', keys=filters) - filters = self._force_key_as_list(filters) - filters = {f: f for f in filters} - # if filters.keys()[0] != 'no_filter': - # msg = ("Warning: pandas-based filtering will be deprecated in the " - # "future!\nPlease switch to quantipy-logic expressions.") - # print UserWarning(msg) - else: - qplogic_filter = True - - if not variables is None: - if not x is None or not y is None: - raise ValueError( - "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " - "at the same time." - ) - - x = self._force_key_as_list(x) - y = self._force_key_as_list(y) - - # Get the lazy y keys none were given and there is only 1 x key - if not x is None: - if len(x)==1 and y is None: - y = self.describe( - index=['y'], - query="x=='%s'" % (x[0]) - ).index.tolist() - - # Get the lazy x keys none were given and there is only 1 y key - if not y is None: - if len(y)==1 and x is None: - x = self.describe( - index=['x'], - query="y=='%s'" % (y[0]) - ).index.tolist() - - for dk in data_keys: - self._verify_key_exists(dk) - for filter_def, logic in filters.items(): - # qp.OPTIONS-based hack to allow faster stack filters - # ------------------------------------------------------ - if qp.OPTIONS['fast_stack_filters']: - if not filter_def in self[dk].keys(): - if filter_def=='no_filter': - self[dk][filter_def].data = self[dk].data - self[dk][filter_def].meta = self[dk].meta - else: - if not qplogic_filter: - try: - self[dk][filter_def].data = self[dk].data.query(logic) - self[dk][filter_def].meta = self[dk].meta - except Exception, ex: - raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - else: - dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) - f_dataset = dataset.filter(filter_def, logic, inplace=False) - self[dk][filter_def].data = f_dataset._data - self[dk][filter_def].meta = f_dataset._meta - else: - if filter_def=='no_filter': - self[dk][filter_def].data = self[dk].data - self[dk][filter_def].meta = self[dk].meta - else: - if not qplogic_filter: - try: - self[dk][filter_def].data = self[dk].data.query(logic) - self[dk][filter_def].meta = self[dk].meta - except Exception, ex: - raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - else: - dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) - f_dataset = dataset.filter(filter_def, logic, inplace=False) - self[dk][filter_def].data = f_dataset._data - self[dk][filter_def].meta = f_dataset._meta - fdata = self[dk][filter_def].data - - if len(fdata) == 0: - raise UserWarning('A filter definition resulted in no cases and will be skipped: {filter_def}'.format(filter_def=filter_def)) - continue - self.__create_links(data=fdata, data_key=dk, the_filter=filter_def, x=x, y=y, views=views, weights=weights, variables=variables) - - def describe(self, index=None, columns=None, query=None, split_view_names=False): - """ - Generates a structured overview of all Link defining Stack elements. - - Parameters - ---------- - index, columns : str of or list of {'data', 'filter', 'x', 'y', 'view'}, - optional - Controls the output representation by structuring a pivot-style - table according to the index and column values. - query : str - A query string that is valid for the pandas.DataFrame.query() method. - split_view_names : bool, default False - If True, will create an output of unique view name notations split - up into their components. - - Returns - ------- - description : pandas.DataFrame - DataFrame summing the Stack's structure in terms of Links and Views. - """ - stack_tree = [] - for dk in self.keys(): - path_dk = [dk] - filters = self[dk] - -# for fk in filters.keys(): -# path_fk = path_dk + [fk] -# xs = self[dk][fk] - - for fk in filters.keys(): - path_fk = path_dk + [fk] - xs = self[dk][fk] - - for sk in xs.keys(): - path_sk = path_fk + [sk] - ys = self[dk][fk][sk] - - for tk in ys.keys(): - path_tk = path_sk + [tk] - views = self[dk][fk][sk][tk] - - if views.keys(): - for vk in views.keys(): - path_vk = path_tk + [vk, 1] - stack_tree.append(tuple(path_vk)) - else: - path_vk = path_tk + ['|||||', 1] - stack_tree.append(tuple(path_vk)) - - column_names = ['data', 'filter', 'x', 'y', 'view', '#'] - description = pd.DataFrame.from_records(stack_tree, columns=column_names) - if split_view_names: - views_as_series = pd.DataFrame( - description.pivot_table(values='#', columns='view', aggfunc='count') - ).reset_index()['view'] - parts = ['xpos', 'agg', 'condition', 'rel_to', 'weights', - 'shortname'] - description = pd.concat( - (views_as_series, - pd.DataFrame(views_as_series.str.split('|').tolist(), - columns=parts)), axis=1) - - description.replace('|||||', np.NaN, inplace=True) - if query is not None: - description = description.query(query) - if not index is None or not columns is None: - description = description.pivot_table(values='#', index=index, columns=columns, - aggfunc='count') - return description - - def refresh(self, data_key, new_data_key='', new_weight=None, - new_data=None, new_meta=None): - """ - Re-run all or a portion of Stack's aggregations for a given data key. - - refresh() can be used to re-weight the data using a new case data - weight variable or to re-run all aggregations based on a changed source - data version (e.g. after cleaning the file/ dropping cases) or a - combination of the both. - - .. note:: - Currently this is only supported for the preset QuantipyViews(), - namely: ``'cbase'``, ``'rbase'``, ``'counts'``, ``'c%'``, - ``'r%'``, ``'mean'``, ``'ebase'``. - - Parameters - ---------- - data_key : str - The Links' data key to be modified. - new_data_key : str, default '' - Controls if the existing data key's files and aggregations will be - overwritten or stored via a new data key. - new_weight : str - The name of a new weight variable used to re-aggregate the Links. - new_data : pandas.DataFrame - The case data source. If None is given, the - original case data found for the data key will be used. - new_meta : quantipy meta document - A meta data source associated with the case data. If None is given, - the original meta definition found for the data key will be used. - - Returns - ------- - None - """ - content = self.describe()[['data', 'filter', 'x', 'y', 'view']] - content = content[content['data'] == data_key] - put_meta = self[data_key].meta if new_meta is None else new_meta - put_data = self[data_key].data if new_data is None else new_data - dk = new_data_key if new_data_key else data_key - self.add_data(data_key=dk, data=put_data, meta=put_meta) - skipped_views = [] - for _, f, x, y, view in content.values: - shortname = view.split('|')[-1] - if shortname not in ['default', 'cbase', 'cbase_gross', - 'rbase', 'counts', 'c%', - 'r%', 'ebase', 'mean', - 'c%_sum', 'counts_sum']: - if view not in skipped_views: - skipped_views.append(view) - warning_msg = ('\nOnly preset QuantipyViews are supported.' - 'Skipping: {}').format(view) - print warning_msg - else: - view_weight = view.split('|')[-2] - if not x in [view_weight, new_weight]: - if new_data is None and new_weight is not None: - if not view_weight == '': - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - if new_weight == '': - weight = None - else: - weight = [None, new_weight] - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - else: - if view_weight == '': - weight = None - elif new_weight is not None: - if not (view_weight == new_weight): - if new_weight == '': - weight = [None, view_weight] - else: - weight = [view_weight, new_weight] - else: - weight = view_weight - else: - weight = view_weight - try: - self.add_link(data_keys=dk, filters=f, x=x, y=y, - weights=weight, views=[shortname]) - except ValueError, e: - print '\n', e - return None - - def save(self, path_stack, compression="gzip", store_cache=True, - decode_str=False, dataset=False, describe=False): - """ - Save Stack instance to .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The intended compression type. - store_cache : bool, default True - Stores the MatrixCache in a file in the same location. - decode_str : bool, default=True - If True the unicoder function will be used to decode all str - objects found anywhere in the meta document/s. - dataset : bool, default=False - If True a json/csv will be saved parallel to the saved stack - for each data key in the stack. - describe : bool, default=False - If True the result of stack.describe().to_excel() will be - saved parallel to the saved stack. - - Returns - ------- - None - """ - protocol = cPickle.HIGHEST_PROTOCOL - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.save() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.save(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.save(path_stack='%s', ...)" % (path_stack) - ) - - # Make sure there are no str objects in any meta documents. If - # there are any non-ASCII characters will be encoded - # incorrectly and lead to UnicodeDecodeErrors in Jupyter. - if decode_str: - for dk in self.keys(): - self[dk].meta = unicoder(self[dk].meta) - - if compression is None: - f = open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - else: - f = gzip.open(path_stack, 'wb') - cPickle.dump(self, f, protocol) - - if store_cache: - caches = {} - for key in self.keys(): - caches[key] = self[key].cache - - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f1 = open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - else: - f1 = gzip.open(path_cache, 'wb') - cPickle.dump(caches, f1, protocol) - - f1.close() - - f.close() - - if dataset: - for key in self.keys(): - path_json = path_stack.replace( - '.stack', - ' [{}].json'.format(key)) - path_csv = path_stack.replace( - '.stack', - ' [{}].csv'.format(key)) - write_quantipy( - meta=self[key].meta, - data=self[key].data, - path_json=path_json, - path_csv=path_csv) - - if describe: - path_describe = path_stack.replace('.stack', '.xlsx') - self.describe().to_excel(path_describe) - - # def get_slice(data_key=None, x=None, y=None, filters=None, views=None): - # """ """ - # pass - - # STATIC METHODS - - @staticmethod - def from_sav(data_key, filename, name=None, path=None, ioLocale="en_US.UTF-8", ioUtf8=True): - """ - Creates a new stack instance from a .sav file. - - Parameters - ---------- - data_key : str - The data_key for the data and meta in the sav file. - filename : str - The name to the sav file. - name : str - A name for the sav (stored in the meta). - path : str - The path to the sav file. - ioLocale : str - The locale used in during the sav processing. - ioUtf8 : bool - Boolean that indicates the mode in which text communicated to or - from the I/O module will be. - - Returns - ------- - stack : stack object instance - A stack instance that has a data_key with data and metadata - to run aggregations. - """ - if name is None: - name = data_key - - meta, data = parse_sav_file(filename=filename, path=path, name=name, ioLocale=ioLocale, ioUtf8=ioUtf8) - return Stack(add_data={name: {'meta': meta, 'data':data}}) - - @staticmethod - def load(path_stack, compression="gzip", load_cache=False): - """ - Load Stack instance from .stack file. - - Parameters - ---------- - path_stack : str - The full path to the .stack file that should be created, including - the extension. - compression : {'gzip'}, default 'gzip' - The compression type that has been used saving the file. - load_cache : bool, default False - Loads MatrixCache into the Stack a .cache file is found. - - Returns - ------- - None - """ - - - if not path_stack.endswith('.stack'): - raise ValueError( - "To avoid ambiguity, when using Stack.load() you must provide the full path to " - "the stack file you want to create, including the file extension. For example: " - "stack.load(path_stack='./output/MyStack.stack'). Your call looks like this: " - "stack.load(path_stack='%s', ...)" % (path_stack) - ) - - if compression is None: - f = open(path_stack, 'rb') - else: - f = gzip.open(path_stack, 'rb') - new_stack = cPickle.load(f) - f.close() - - if load_cache: - path_cache = path_stack.replace('.stack', '.cache') - if compression is None: - f = open(path_cache, 'rb') - else: - f = gzip.open(path_cache, 'rb') - caches = cPickle.load(f) - for key in caches.keys(): - if key in new_stack.keys(): - new_stack[key].cache = caches[key] - else: - raise ValueError( - "Tried to insert a loaded MatrixCache in to a data_key in the stack that" - "is not in the stack. The data_key is '{}', available keys are {}" - .format(key, caches.keys()) - ) - f.close() - - return new_stack - - - # PRIVATE METHODS - - def __get_all_y_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].y_variables - else: - raise KeyError("get_all_y_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys(self, data_key, the_filter="no_filter"): - if(self.stack_pos == 'stack_root'): - return self[data_key].x_variables - else: - raise KeyError("get_all_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __get_all_x_keys_except(self, data_key, exception): - keys = self.__get_all_x_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __get_all_y_keys_except(self, data_key, exception): - keys = self.__get_all_y_keys(data_key) - return [i for i in keys if i != exception[0]] - - def __set_x_key(self, key): - if self.x_variables is None: - self.x_variables = set(key) - else: - self.x_variables.update(key) - - def __set_y_key(self, key): - if self.y_variables is None: - self.y_variables = set(key) - else: - self.y_variables.update(key) - - def _set_x_and_y_keys(self, data_key, x, y): - """ - Sets the x_variables and y_variables in the data part of the stack for this data_key, e.g. stack['Jan']. - This method can also be used to add to the current lists and it makes sure the list stays unique. - """ - if self.stack_pos == 'stack_root': - self[data_key].__set_x_key(x) - self[data_key].__set_y_key(y) - else: - raise KeyError("set_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) - - def __create_combinations(self, data, data_key, x=None, y=None, weight=None, variables=None): - if isinstance(y, str): - y = [y] - if isinstance(x, str): - x = [x] - - has_metadata = self[data_key].meta is not None and not isinstance(self[data_key].meta, Stack) - - # any(...) returns true if ANY of the vars are not None - if any([x, y]) and variables is not None: - # Raise an error if variables AND x/y are BOTH supplied - raise ValueError("Either use the 'variables' OR 'x', 'y' NOT both.") - - if not any([x, y]): - if variables is None: - if not has_metadata: - # "fully-lazy" method. (variables, x and y are all None) - variables = data.columns.tolist() - - if variables is not None: - x = variables - y = variables - variables = None - - # Ensure that we actually have metadata - if has_metadata: - # THEN we try to create the combinations with metadata - combinations = self.__create_combinations_with_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - else: - # Either variables or both x AND y are supplied. Then create the combinations from that. - combinations = self.__create_combinations_no_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) - - unique_list = set([item for comb in combinations for item in comb]) - - return combinations, unique_list - - def __create_combinations_with_meta(self, data, data_key, x=None, y=None, weight=None): - # TODO: These meta functions should possibly be in the helpers functions - metadata_columns = self[data_key].meta['columns'].keys() - for mask, mask_data in self[data_key].meta['masks'].iteritems(): - # TODO :: Get the static list from somewhere. not hardcoded. - if mask_data['type'].lower() in ['array', 'dichotomous set', - "categorical set"]: - metadata_columns.append(mask) - for item in mask_data['items']: - if "source" in item: - column = item["source"].split('@')[1] - metadata_columns.remove(column) - elif mask_data['type'].lower() in ["overlay"]: - pass - # Use all from the metadata, if nothing is specified (fully-lazy) - if x is None and y is None: - x = metadata_columns - y = metadata_columns - if all([x, y]): - metadata_columns = list(set(metadata_columns + x + y)) - elif x is not None: - metadata_columns = list(set(metadata_columns + x)) - elif y is not None: - metadata_columns = list(set(metadata_columns + y)) - combinations = functions.create_combinations_from_array(sorted(metadata_columns)) - - for var in [x, y]: - if var is not None: - if weight in var: - var.remove(weight) - if all([x, y]): - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x and y_item in y] - elif x is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if x_item in x] - elif y is not None: - combinations = [(x_item, y_item) for x_item, y_item in combinations - if y_item in y] - - return combinations - - def __create_combinations_no_meta(self, data, data_key, x=None, y=None, weight=None): - if x is None: - x = data.columns.tolist() - if y is None: - y = data.columns.tolist() - for var in [x, y]: - if weight in var: - var.remove(weight) - combinations = [(x_item, y_item) for x_item in x for y_item - in y if x_item != y_item] - self._set_x_and_y_keys(data_key, x, y) - - return combinations - - def __create_links(self, data, data_key, views, variables=None, x=None, y=None, - the_filter=None, store_view_in_link=False, weights=None): - if views is not None: - has_links = True if self[data_key][the_filter].keys() else False - if has_links: - xs = self[data_key][the_filter].keys() - if x is not None: - valid_x = [xk for xk in xs if xk in x] - valid_x.extend(x) - x = set(valid_x) - else: - x = xs - ys = list(set(itertools.chain.from_iterable( - [self[data_key][the_filter][xk].keys() - for xk in xs]))) - if y is not None: - valid_y = [yk for yk in ys if yk in y] - valid_y.extend(y) - y = set(valid_y) - else: - y = ys - if self._x_and_y_keys_in_file(data_key, data, x, y): - for x_key, y_key in itertools.product(x, y): - if x_key==y_key and x_key=='@': - continue - if y_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y='@', - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key]['@'] = link - else: - link = self[data_key][the_filter][x_key]['@'] - elif x_key == '@': - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x='@', - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter]['@'][y_key] = link - else: - link = self[data_key][the_filter]['@'][y_key] - else: - if not isinstance(self[data_key][the_filter][x_key][y_key], Link): - link = Link( - the_filter=the_filter, - x=x_key, - y=y_key, - data_key=data_key, - stack=self, - store_view=store_view_in_link, - create_views=False - ) - self[data_key][the_filter][x_key][y_key] = link - else: - link = self[data_key][the_filter][x_key][y_key] - if views is not None: - views._apply_to(link, weights) - - def _x_and_y_keys_in_file(self, data_key, data, x, y): - data_columns = data.columns.tolist() - if '>' in ','.join(y): y = self._clean_from_nests(y) - if '>' in ','.join(x): - raise NotImplementedError('x-axis Nesting not supported.') - x_not_found = [var for var in x if not var in data_columns - and not var == '@'] - y_not_found = [var for var in y if not var in data_columns - and not var == '@'] - if x_not_found is not None: - masks_meta_lookup_x = [var for var in x_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_x: - x_not_found.remove(found_in_meta) - if y_not_found is not None: - masks_meta_lookup_y = [var for var in y_not_found - if var in self[data_key].meta['masks'].keys()] - for found_in_meta in masks_meta_lookup_y: - y_not_found.remove(found_in_meta) - if not x_not_found and not y_not_found: - return True - elif x_not_found and y_not_found: - raise ValueError( - 'data key {}: x: {} and y: {} not found.'.format( - data_key, x_not_found, y_not_found)) - elif x_not_found: - raise ValueError( - 'data key {}: x: {} not found.'.format( - data_key, x_not_found)) - elif y_not_found: - raise ValueError( - 'data key {}: y: {} not found.'.format( - data_key, y_not_found)) - - def _clean_from_nests(self, variables): - cleaned = [] - nests = [var for var in variables if '>' in var] - non_nests = [var for var in variables if not '>' in var] - for nest in nests: - cleaned.extend([var.strip() for var in nest.split('>')]) - non_nests += cleaned - non_nests = list(set(non_nests)) - return non_nests - - def __clean_column_names(self, columns): - """ - Remove extra doublequotes if there are any - """ - cols = [] - for column in columns: - cols.append(column.replace('"', '')) - return cols - - def __generate_key_from_list_of(self, list_of_keys): - """ - Generate keys from a list (or tuple). - """ - list_of_keys = list(list_of_keys) - list_of_keys.sort() - return ",".join(list_of_keys) - - def __has_list(self, small): - """ - Check if object contains a list of strings. - """ - keys = self.keys() - for i in xrange(len(keys)-len(small)+1): - for j in xrange(len(small)): - if keys[i+j] != small[j]: - break - else: - return i, i+len(small) - return False - - def __get_all_combinations(self, list_of_items): - """Generates all combinations of items from a list """ - return [itertools.combinations(list_of_items, index+1) - for index in range(len(list_of_items))] - - def __get_stack_pointer(self, stack_pos): - """Takes a stack_pos and returns the stack with that location - raises an exception IF the stack pointer is not found - """ - if self.parent.stack_pos == stack_pos: - return self.parent - else: - return self.parent.__get_stack_pointer(stack_pos) - - def __get_chains(self, name, data_keys, filters, x, y, views, - orientation, select, rules, - rules_weight): - """ - List comprehension wrapper around .get_chain(). - """ - if orientation == 'y': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x, - y=y_var, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for y_var in y - ] - elif orientation == 'x': - return [ - self.get_chain( - name=name, - data_keys=data_keys, - filters=filters, - x=x_var, - y=y, - views=views, - select=select, - rules=rules, - rules_weight=rules_weight - ) - for x_var in x - ] - else: - raise ValueError( - "Unknown orientation type. Please use 'x' or 'y'." - ) - - def _verify_multiple_key_types(self, data_keys=None, filters=None, x=None, - y=None, variables=None, views=None): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if data_keys is not None: - self._verify_key_types(name='data', keys=data_keys) - - if filters is not None: - self._verify_key_types(name='filter', keys=filters) - - if x is not None: - self._verify_key_types(name='x', keys=x) - - if y is not None: - self._verify_key_types(name='y', keys=y) - - if variables is not None: - self._verify_key_types(name='variables', keys=variables) - - if views is not None: - self._verify_key_types(name='view', keys=views) - - def _verify_key_exists(self, key, stack_path=[]): - """ - Verify that the given key exists in the stack at the path targeted. - """ - error_msg = ( - "Could not find the {key_type} key '{key}' in: {stack_path}. " - "Found {keys_found} instead." - ) - try: - dk = stack_path[0] - fk = stack_path[1] - xk = stack_path[2] - yk = stack_path[3] - vk = stack_path[4] - except: - pass - try: - if len(stack_path) == 0: - if key not in self: - key_type, keys_found = 'data', self.keys() - stack_path = 'stack' - raise ValueError - elif len(stack_path) == 1: - if key not in self[dk]: - key_type, keys_found = 'filter', self[dk].keys() - stack_path = "stack['{dk}']".format( - dk=dk) - raise ValueError - elif len(stack_path) == 2: - if key not in self[dk][fk]: - key_type, keys_found = 'x', self[dk][fk].keys() - stack_path = "stack['{dk}']['{fk}']".format( - dk=dk, fk=fk) - raise ValueError - elif len(stack_path) == 3: - meta = self[dk].meta - if self._is_array_summary(meta, xk, None) and not key == '@': - pass - elif key not in self[dk][fk][xk]: - key_type, keys_found = 'y', self[dk][fk][xk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']".format( - dk=dk, fk=fk, xk=xk) - raise ValueError - elif len(stack_path) == 4: - if key not in self[dk][fk][xk][yk]: - key_type, keys_found = 'view', self[dk][fk][xk][yk].keys() - stack_path = "stack['{dk}']['{fk}']['{xk}']['{yk}']".format( - dk=dk, fk=fk, xk=xk, yk=yk) - raise ValueError - except ValueError: - print error_msg.format( - key_type=key_type, - key=key, - stack_path=stack_path, - keys_found=keys_found - ) - - def _force_key_as_list(self, key): - """Returns key as [key] if it is str or unicode""" - return [key] if isinstance(key, (str, unicode)) else key - - def _verify_key_types(self, name, keys): - """ - Verify that the given keys str or unicode or a list or tuple of those. - """ - if isinstance(keys, (list, tuple)): - for key in keys: - self._verify_key_types(name, key) - elif isinstance(keys, (str, unicode)): - pass - else: - raise TypeError( - "All %s keys must be one of the following types: " - " or , " - " of or , " - " of or . " - "Given: %s" % (name, keys) - ) - - def _find_groups(self, view): - groups = OrderedDict() - logic = view._kwargs.get('logic') - description = view.describe_block() - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - net_names = [v for v, d in description.items() if d == 'net'] - for l in logic: - new_l = copy.deepcopy(l) - for k in l: - if k not in net_names: - del new_l[k] - groups[new_l.keys()[0]] = new_l.values()[0] - groups['codes'] = [c for c, d in description.items() if d == 'normal'] - return groups - - def sort_expanded_nets(self, view, within=True, between=True, ascending=False, - fix=None): - if not within and not between: - return view.dataframe - df = view.dataframe - name = df.index.levels[0][0] - if not fix: - fix_codes = [] - else: - if not isinstance(fix, list): - fix_codes = [fix] - else: - fix_codes = fix - fix_codes = [c for c in fix_codes if c in - df.index.get_level_values(1).tolist()] - net_groups = self._find_groups(view) - sort_col = (df.columns.levels[0][0], '@') - sort = [(name, v) for v in df.index.get_level_values(1) - if (v in net_groups['codes'] or - v in net_groups.keys()) and not v in fix_codes] - if between: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - between_order = temp_df.index.get_level_values(1).tolist() - code_group_list = [] - for g in between_order: - if g in net_groups: - code_group_list.append([g] + net_groups[g]) - elif g in net_groups['codes']: - code_group_list.append([g]) - final_index = [] - for g in code_group_list: - is_code = len(g) == 1 - if not is_code: - fixed_net_name = g[0] - sort = [(name, v) for v in g[1:]] - if within: - if pd.__version__ == '0.19.2': - temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) - else: - temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) - else: - temp_df = df.loc[sort] - new_idx = [fixed_net_name] + temp_df.index.get_level_values(1).tolist() - final_index.extend(new_idx) - else: - final_index.extend(g) - final_index = [(name, i) for i in final_index] - if fix_codes: - fix_codes = [(name, f) for f in fix_codes] - final_index.extend(fix_codes) - df = df.reindex(final_index) - return df - - def get_frequency_via_stack(self, data_key, the_filter, col, weight=None): - weight_notation = '' if weight is None else weight - vk = 'x|f|:||{}|counts'.format(weight_notation) - try: - f = self[data_key][the_filter][col]['@'][vk].dataframe - except (KeyError, AttributeError) as e: - try: - f = self[data_key][the_filter]['@'][col][vk].dataframe.T - except (KeyError, AttributeError) as e: - f = frequency(self[data_key].meta, self[data_key].data, x=col, weight=weight) - return f - - def get_descriptive_via_stack(self, data_key, the_filter, col, weight=None): - l = self[data_key][the_filter][col]['@'] - w = '' if weight is None else weight - mean_key = [k for k in l.keys() if 'd.mean' in k.split('|')[1] and - k.split('|')[-2] == w] - if not mean_key: - msg = "No mean view to sort '{}' on found!" - raise RuntimeError(msg.format(col)) - elif len(mean_key) > 1: - msg = "Multiple mean views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - mean_key = mean_key[0] - vk = mean_key - d = l[mean_key].dataframe - return d - - def _is_array_summary(self, meta, x, y): - return x in meta['masks'] - - def _is_transposed_summary(self, meta, x, y): - return x == '@' and y in meta['masks'] - - def axis_slicer_from_vartype(self, all_rules_axes, rules_axis, dk, the_filter, x, y, rules_weight): - if rules_axis == 'x' and 'x' not in all_rules_axes: - return None - elif rules_axis == 'y' and 'y' not in all_rules_axes: - return None - meta = self[dk].meta - - array_summary = self._is_array_summary(meta, x, y) - transposed_summary = self._is_transposed_summary(meta, x, y) - - axis_slicer = None - - if rules_axis == 'x': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=True) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - elif rules_axis == 'y': - if not array_summary and not transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, y=y, weight=rules_weight) - elif array_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x=x, y='@', weight=rules_weight, - slice_array_items=False) - elif transposed_summary: - axis_slicer = self.get_rules_slicer_via_stack( - dk, the_filter, x='@', y=y, weight=rules_weight) - - return axis_slicer - - def get_rules_slicer_via_stack(self, data_key, the_filter, - x=None, y=None, weight=None, - slice_array_items=False): - m = self[data_key].meta - array_summary = self._is_array_summary(m, x, y) - transposed_summary = self._is_transposed_summary(m, x, y) - - rules = None - - if not array_summary and not transposed_summary: - if not x is None: - try: - rules = self[data_key].meta['columns'][x]['rules']['x'] - col = x - except: - pass - elif not y is None: - try: - rules = self[data_key].meta['columns'][y]['rules']['y'] - col = y - except: - pass - - elif array_summary: - if slice_array_items: - try: - rules = self[data_key].meta['masks'][x]['rules']['x'] - col = x - except: - pass - else: - try: - rules = self[data_key].meta['masks'][x]['rules']['y'] - col = x - except: - pass - - elif transposed_summary: - try: - rules = self[data_key].meta['masks'][y]['rules']['x'] - col = y - except: - pass - - if not rules: return None - views = self[data_key][the_filter][col]['@'].keys() - w = '' if weight is None else weight - expanded_net = [v for v in views if '}+]' in v - and v.split('|')[-2] == w - and v.split('|')[1] == 'f' and - not v.split('|')[3] == 'x'] - if expanded_net: - if len(expanded_net) > 1: - if len(expanded_net) == 2: - if expanded_net[0].split('|')[2] == expanded_net[1].split('|')[2]: - expanded_net = expanded_net[0] - else: - msg = "Multiple 'expand' using views found for '{}'. Unable to sort!" - raise RuntimeError(msg.format(col)) - else: - expanded_net = expanded_net[0] - if 'sortx' in rules: - on_mean = rules['sortx'].get('sort_on', '@') == 'mean' - else: - on_mean = False - if 'sortx' in rules and on_mean: - f = self.get_descriptive_via_stack( - data_key, the_filter, col, weight=weight) - elif 'sortx' in rules and expanded_net: - within = rules['sortx'].get('within', False) - between = rules['sortx'].get('between', False) - fix = rules['sortx'].get('fixed', False) - ascending = rules['sortx'].get('ascending', False) - view = self[data_key][the_filter][col]['@'][expanded_net] - f = self.sort_expanded_nets(view, between=between, within=within, - ascending=ascending, fix=fix) - else: - f = self.get_frequency_via_stack( - data_key, the_filter, col, weight=weight) - - if transposed_summary or (not slice_array_items and array_summary): - rules_slicer = functions.get_rules_slicer(f.T, rules) - else: - if not expanded_net or ('sortx' in rules and on_mean): - rules_slicer = functions.get_rules_slicer(f, rules) - else: - rules_slicer = f.index.values.tolist() - try: - rules_slicer.remove((col, 'All')) - except: - pass - return rules_slicer - - @modify(to_list='batches') - def _check_batches(self, dk, batches='all'): - """ - Returns a list of valid ``qp.Batch`` names. - - Parameters - ---------- - batches: str/ list of str, default 'all' - Included names are checked against valid ``qp.Batch`` names. If - batches='all', all valid ``Batch`` names are returned. - - Returns - ------- - list of str - """ - if not batches: - return [] - elif batches[0] == 'all': - return self[dk].meta['sets']['batches'].keys() - else: - valid = self[dk].meta['sets']['batches'].keys() - not_valid = [b for b in batches if not b in valid] - if not_valid: - msg = '``Batch`` name not found in ``Stack``: {}' - raise KeyError(msg.format(not_valid)) - return batches - - def _x_y_f_w_map(self, dk, batches='all'): - """ - """ - def _append_loop(mapping, x, fi, w, ys): - if fi: fi = fi.encode('utf8') - fn = 'no_filter' if fi is None else fi - f = 'no_filter' if fi is None else {fi: {fi: 0}} - if not x in mapping: - mapping[x] = {fn: {'f': f, tuple(w): ys}} - elif not fn in mapping[x]: - mapping[x][fn] = {'f': f, tuple(w): ys} - elif not tuple(w) in mapping[x][fn]: - mapping[x][fn][tuple(w)] = ys - elif not all(y in mapping[x][fn][tuple(w)] for y in ys): - yks = set(mapping[x][fn][tuple(w)]).union(set(ys)) - mapping[x][fn][tuple(w)] = list(yks) - return None - - arrays = self.variable_types(dk, verbose=False)['array'] - mapping = {} - y_on_y = {} - batches = self._check_batches(dk, batches) - for batch in batches: - b = self[dk].meta['sets']['batches'][batch] - xy = b['x_y_map'] - f = b['x_filter_map'] - fy = b['y_filter_map'] - w = b['weights'] - for x, y in xy: - if x == '@': - y = y[0] - _append_loop(mapping, x, f[y], w, [y]) - else: - _append_loop(mapping, x, f[x], w, y) - for yy in b['y_on_y']: - for x in b['yks'][1:]: - _append_loop(mapping, x, fy[yy], w, b['yks']) - _append_loop(y_on_y, x, fy[yy], w, b['yks']) - return mapping, y_on_y - - @modify(to_list=['views', 'categorize', 'xs', 'batches']) - def aggregate(self, views, unweighted_base=True, categorize=[], - batches='all', xs=None, bases={}, verbose=True): - - """ - Add views to all defined ``qp.Link`` in ``qp.Stack``. - - Parameters - ---------- - views: str or list of str or qp.ViewMapper - ``views`` that are added. - unweighted_base: bool, default True - If True, unweighted 'cbase' is added to all non-arrays. - This parameter will be deprecated in future, please use bases - instead. - categorize: str or list of str - Determines how numerical data is handled: If provided, the - variables will get counts and percentage aggregations - (``'counts'``, ``'c%'``) alongside the ``'cbase'`` view. If False, - only ``'cbase'`` views are generated for non-categorical types. - batches: str/ list of str, default 'all' - Name(s) of ``qp.Batch`` instance(s) that are used to aggregate the - ``qp.Stack``. - xs: list of str - Names of variable, for which views are added. - bases: dict - Defines which bases should be aggregated, weighted or unweighted. - - Returns - ------- - None, modify ``qp.Stack`` inplace - """ - # Preparing bases if older version with unweighed_base is used - valid_bases = ['cbase', 'cbase_gross', 'ebase'] - if not bases and any(v in valid_bases for v in views): - new_bases = {} - for ba in valid_bases: - if ba in views: - new_bases[ba] = {'unwgt': False if ba=='ebase' else unweighted_base, - 'wgt': True} - views = [v for v in views if not v in valid_bases] - else: - new_bases = bases - - # Check if views are complete - if views and isinstance(views[0], ViewMapper): - views = views[0] - complete = views[views.keys()[0]]['kwargs'].get('complete', False) - elif any('cumsum' in v for v in views): - complete = True - else: - complete = False - - # get counts + net views - count_net_views = ['counts', 'counts_sum', 'counts_cumsum'] - if isinstance(views, ViewMapper) and views.keys() == ['net']: - counts_nets = qp.ViewMapper() - counts_nets.make_template('frequency', {'rel_to': [None, 'y']}) - options = {'logic': views['net']['kwargs']['logic'], - 'axis': 'x', - 'expand': views['net']['kwargs']['expand'], - 'complete': views['net']['kwargs']['complete'], - 'calc': views['net']['kwargs']['calc']} - counts_nets.add_method('net', kwargs=options) - else: - counts_nets = [v for v in views if v in count_net_views] - - x_in_stack = self.describe('x').index.tolist() - for dk in self.keys(): - batches = self._check_batches(dk, batches) - if not batches: return None - # check for unweighted_counts - batch = self[dk].meta['sets']['batches'] - unwgt_c = any(batch[b].get('unwgt_counts') for b in batches) - # get map and conditions for aggregation - x_y_f_w_map, y_on_y = self._x_y_f_w_map(dk, batches) - if not xs: - xs = [x for x in x_y_f_w_map.keys() if x in x_in_stack] - else: - xs = [x for x in xs if x in x_in_stack or isinstance(x, tuple)] - v_typ = self.variable_types(dk, verbose=False) - numerics = v_typ['int'] + v_typ['float'] - masks = self[dk].meta['masks'] - num_arrays = [m for m in masks if masks[m]['subtype'] in ['int', 'float']] - if num_arrays: numerics = numerics + num_arrays - skipped = [x for x in xs if (x in numerics and not x in categorize) - and not isinstance(x, tuple)] - total_len = len(xs) - # loop over map and aggregate views - if total_len == 0: - msg = "Cannot aggregate, 'xs' contains no valid variables." - raise ValueError(msg) - for idx, x in enumerate(xs, start=1): - y_trans = None - if isinstance(x, tuple): - y_trans = x[1] - x = x[0] - if not x in x_y_f_w_map.keys(): - msg = "Cannot find {} in qp.Stack for ``qp.Batch`` '{}'" - raise KeyError(msg.format(x, batches)) - v = [] if x in skipped else views - for f_dict in x_y_f_w_map[x].values(): - f = f_dict['f'] - f_key = f.keys()[0] if isinstance(f, dict) else f - for weight, y in f_dict.items(): - if weight == 'f': continue - if y_trans: y = y_trans - w = list(weight) if weight else None - # add bases - for ba, weights in new_bases.items(): - ba_w = [b_w for b_w in w if not b_w is None] - if weights.get('wgt') and ba_w: - self.add_link(dk, f, x=x, y=y, views=[ba], weights=ba_w) - if (weights.get('wgt') and not ba_w) or weights.get('unwgt'): - self.add_link(dk, f, x=x, y=y, views=[ba], weights=None) - # remove existing nets for link if new view is a net - if isinstance(v, ViewMapper) and v.get('net') and not y_trans: - for ys in y: - link = self[dk][f_key][x][ys] - for view in link.keys(): - is_net = view.split('|')[-1] == 'net' - has_w = view.split('|')[-2] - if not has_w: has_w = None - if is_net and has_w in f_dict.keys(): - del link[view] - # add unweighted views for counts/ nets - if unwgt_c and counts_nets and not None in w: - self.add_link(dk, f, x=x, y=y, views=counts_nets, weights=None) - # add common views - self.add_link(dk, f, x=x, y=y, views=v, weights=w) - # remove views if complete (cumsum/ nets) - if complete: - for ys in y: - y_on_ys = y_on_y.get(x, {}).get(f_key, {}).get(tuple(w), []) - if ys in y_on_ys: continue - link = self[dk][f_key][x][ys] - for ws in w: - pct = 'x|f|:|y|{}|c%'.format('' if not ws else ws) - counts = 'x|f|:||{}|counts'.format('' if not ws else ws) - for view in [pct, counts]: - if view in link: - del link[view] - if verbose: - done = float(idx) / float(total_len) *100 - print '\r', - time.sleep(0.01) - print 'Stack [{}]: {} %'.format(dk, round(done, 1)), - sys.stdout.flush() - print '\n' - - if skipped and verbose: - msg = ("\n\nWarning: Found {} non-categorized numeric variable(s): {}.\n" - "Descriptive statistics must be added!") - print msg.format(len(skipped), skipped) - return None - - @modify(to_list=['on_vars', '_batches']) - def cumulative_sum(self, on_vars, _batches='all', verbose=True): - """ - Add cumulative sum view to a specified collection of xks of the stack. - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - The stack instance is modified inplace. - """ - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches or not on_vars: return None - meta = self[dk].meta - data = self[dk].data - for v in on_vars: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars + items)) - - self.aggregate(['counts_cumsum', 'c%_cumsum'], False, [], _batches, on_vars, verbose=verbose) - return None - - def _add_checking_chain(self, dk, cluster, name, x, y, views): - key, view, c_view = views - if isinstance(cluster, ChainManager): - c_stack = cluster.stack - else: - c_stack = qp.Stack('checks') - c_stack.add_data('checks', data=self[dk].data, meta=self[dk].meta) - c_stack.add_link(x=x, y=y, views=view, weights=None) - c_stack.add_link(x=x, y=y, views=c_view, weights=None) - c_views = c_stack.describe('view').index.tolist() - len_v_keys = len(view) - view_keys = ['x|f|x:|||cbase', 'x|f|:|||counts'][0:len_v_keys] - c_views = view_keys + [v for v in c_views - if v.endswith('{}_check'.format(key))] - if isinstance(cluster, ChainManager): - cluster.get('checks', 'no_filter', x, y, c_views, folder=name) - else: - if name == 'stat_check': - chain = c_stack.get_chain(x=x, y=y, views=c_views, orient_on='x') - name = [v for v in c_views if v.endswith('{}_check'.format(key))][0] - cluster[name] = chain - else: - chain = c_stack.get_chain(name=name, x=x, y=y, views=c_views) - cluster.add_chain(chain) - return cluster - - @staticmethod - def recode_from_net_def(dataset, on_vars, net_map, expand, recode='auto', - text_prefix='Net:', verbose=True): - """ - Create variables from net definitions. - """ - def _is_simple_net(net_map): - return all(isinstance(net.values()[0], list) for net in net_map) - - def _dissect_defs(ds, var, net_map, recode, text_prefix): - mapper = [] - if recode == 'extend_codes': - mapper += [(x, y, {var: x}) for (x,y) in ds.values(var)] - max_code = max(ds.codes(var)) - elif recode == 'drop_codes': - max_code = 0 - elif 'collect_codes' in recode: - max_code = 0 - - appends = [] - labels = {} - s_net = True - simple_nets = [] - for x, net in enumerate(net_map, 1): - n = copy.deepcopy(net) - if net.get('text'): - labs = n.pop('text') - else: - labs = {ds.text_key: n.keys()[0]} - code = max_code + x - for tk, lab in labs.items(): - if not tk in labels: labels[tk] = {} - labels[tk].update({code: '{} {}'.format(text_prefix, lab)}) - appends.append((code, str(code), {var: n.values()[0]})) - if not isinstance(n.values()[0], list): - s_net = False - simple_nets = [] - if s_net: - simple_nets.append( - ('{} {}'.format(text_prefix, labs[ds.text_key]), n.values()[0])) - mapper += appends - q_type = 'delimited set' if ds._is_delimited_set_mapper(mapper) else 'single' - return mapper, q_type, labels, simple_nets - - forced_recode = False - valid = ['extend_codes', 'drop_codes', 'collect_codes'] - if recode == 'auto': - recode = 'collect_codes' - forced_recode = True - if not any(rec in recode for rec in valid): - raise ValueError("'recode' must be one of {}".format(valid)) - - dataset._meta['sets']['to_array'] = {} - for var in on_vars[:]: - if dataset.is_array(var): continue - # get name for new variable - suffix = '_rc' - for s in [str(x) if not x == 1 else '' for x in frange('1-5')]: - suf = suffix + s - name = '{}{}'.format(dataset._dims_free_arr_item_name(var), suf) - if dataset.var_exists(name): - if dataset._meta['columns'][name]['properties'].get('recoded_net'): - break - else: - break - - # collect array items - if dataset._is_array_item(var): - to_array_set = dataset._meta['sets']['to_array'] - parent = dataset._maskname_from_item(var) - arr_name = dataset._dims_free_arr_name(parent) + suf - if arr_name in dataset: - msg = "Cannot create array {}. Variable already exists!" - if not dataset.get_property(arr_name, 'recoded_net'): - raise ValueError(msg.format(arr_name)) - no = dataset.item_no(var) - if not arr_name in to_array_set: - to_array_set[arr_name] = [parent, [name], [no]] - else: - to_array_set[arr_name][1].append(name) - to_array_set[arr_name][2].append(no) - - # create mapper to derive new variable - mapper, q_type, labels, simple_nets = _dissect_defs( - dataset, var, net_map, recode, text_prefix) - dataset.derive(name, q_type, dataset.text(var), mapper) - - # meta edits for new variable - for tk, labs in labels.items(): - dataset.set_value_texts(name, labs, tk) - text = dataset.text(var, tk) or dataset.text(var, None) - dataset.set_variable_text(name, text, tk) - - # properties - props = dataset._meta['columns'][name]['properties'] - props.update({'recoded_net': var}) - if 'properties' in dataset._meta['columns'][var]: - for pname, prop in dataset._meta['columns'][var]['properties'].items(): - if pname == 'survey': continue - props[pname] = prop - if simple_nets: - props['simple_org_expr'] = simple_nets - - if verbose: - print 'Created: {}'. format(name) - if forced_recode: - warnings.warn("'{}' was a forced recode.".format(name)) - - # order, remove codes - if 'collect_codes' in recode: - other_logic = intersection( - [{var: not_count(0)}, {name: has_count(0)}]) - if dataset._is_array_item(var) or dataset.take(other_logic).tolist(): - cat_name = recode.split('@')[-1] if '@' in recode else 'Other' - code = len(mapper)+1 - dataset.extend_values(name, [(code, str(code))]) - for tk in labels.keys(): - dataset.set_value_texts(name, {code: cat_name}, tk) - dataset.recode(name, {code: other_logic}) - if recode == 'extend_codes' and expand: - codes = dataset.codes(var) - new = [c for c in dataset.codes(name) if not c in codes] - order = [] - remove = [] - for x, y, z in mapper[:]: - if not x in new: - order.append(x) - else: - vals = z.values()[0] - if not isinstance(vals, list): - remove.append(vals) - vals = [vals] - if expand == 'after': - idx = order.index(codes[min([codes.index(v) for v in vals])]) - elif expand == 'before': - idx = order.index(codes[max([codes.index(v) for v in vals])]) + 1 - order.insert(idx, x) - - dataset.reorder_values(name, order) - dataset.remove_values(name, remove) - - for arr_name, arr_items in dataset._meta['sets']['to_array'].items(): - org_mask = arr_items[0] - m_items = arr_items[1] - m_order = arr_items[2] - m_items = [item[1] for item in sorted(zip(m_order, m_items))] - dataset.to_array(arr_name, m_items, '', False) - dims_name = dataset._dims_compat_arr_name(arr_name) - prop = dataset._meta['masks'][dims_name]['properties'] - prop['recoded_net'] = org_mask - if 'properties' in dataset._meta['masks'][org_mask]: - for p, v in dataset._meta['masks'][org_mask]['properties'].items(): - if p == 'survey': continue - prop[p] = v - n_i0 = dataset.sources(dims_name)[0] - simple_net = dataset._meta['columns'][n_i0]['properties'].get('simple_org_expr') - if simple_net: - dataset._meta['masks'][dims_name]['properties'].update( - {'simple_org_expr': simple_net}) - if verbose: - msg = "Array {} built from recoded view variables!" - print msg.format(dims_name) - del dataset._meta['sets']['to_array'] - - return None - - - @modify(to_list=['on_vars', '_batches']) - def add_nets(self, on_vars, net_map, expand=None, calc=None, text_prefix='Net:', - checking_cluster=None, _batches='all', recode='auto', verbose=True): - """ - Add a net-like view to a specified collection of x keys of the stack. - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - net_map : list of dicts - The listed dicts must map the net/band text label to lists of - categorical answer codes to group together, e.g.: - - >>> [{'Top3': [1, 2, 3]}, - ... {'Bottom3': [4, 5, 6]}] - It is also possible to provide enumerated net definition dictionaries - that are explicitly setting ``text`` metadata per ``text_key`` entries: - - >>> [{1: [1, 2], 'text': {'en-GB': 'UK NET TEXT', - ... 'da-DK': 'DK NET TEXT', - ... 'de-DE': 'DE NET TEXT'}}] - expand : {'before', 'after'}, default None - If provided, the view will list the net-defining codes after or before - the computed net groups (i.e. "overcode" nets). - calc : dict, default None - A dictionary that is attaching a text label to a calculation expression - using the the net definitions. The nets are referenced as per - 'net_1', 'net_2', 'net_3', ... . - Supported calculation expressions are add, sub, div, mul. Example: - - >>> {'calc': ('net_1', add, 'net_2'), 'text': {'en-GB': 'UK CALC LAB', - ... 'da-DK': 'DA CALC LAB', - ... 'de-DE': 'DE CALC LAB'}} - text_prefix : str, default 'Net:' - By default each code grouping/net will have its ``text`` label prefixed - with 'Net: '. Toggle by passing None (or an empty str, ''). - checking_cluster : quantipy.Cluster, default None - When provided, an automated checking aggregation will be added to the - ``Cluster`` instance. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - recode: {'extend_codes', 'drop_codes', 'collect_codes', 'collect_codes@cat_name'}, - default 'auto' - Adds variable with nets as codes to DataSet/Stack. If 'extend_codes', - codes are extended with nets. If 'drop_codes', new variable only - contains nets as codes. If 'collect_codes' or 'collect_codes@cat_name' - the variable contains nets and another category that summarises all - codes which are not included in any net. If no cat_name is provided, - 'Other' is taken as default - Returns - ------- - None - The stack instance is modified inplace. - """ - - def _netdef_from_map(net_map, expand, prefix, text_key): - netdef = [] - for no, net in enumerate(net_map, start=1): - if 'text' in net: - logic = net[no] - text = net['text'] - else: - logic = net.values()[0] - text = {t: net.keys()[0] for t in text_key} - if not isinstance(logic, list) and isinstance(logic, int): - logic = [logic] - if prefix and not expand: - text = {k: '{} {}'.format(prefix, v) for k, v in text.items()} - if expand: - text = {k: '{} (NET)'.format(v) for k, v in text.items()} - netdef.append({'net_{}'.format(no): logic, 'text': text}) - return netdef - - def _check_and_update_calc(calc_expression, text_key): - if not isinstance(calc_expression, dict): - err_msg = ("'calc' must be a dict in form of\n" - "{'calculation label': (net # 1, operator, net # 2)}") - raise TypeError(err_msg) - for k, v in calc_expression.items(): - if not k in ['text', 'calc_only']: exp = v - if not k == 'calc_only': text = v - if not 'text' in calc_expression: - text = {tk: text for tk in text_key} - calc_expression['text'] = text - if not isinstance(exp, (tuple, list)) or len(exp) != 3: - err_msg = ("Not properly formed expression found in 'calc':\n" - "{}\nMust be provided as (net # 1, operator, net # 2)") - raise TypeError(err_msg.format(exp)) - return calc_expression - - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - only_recode = not _batches and recode - if not _batches and not recode: return None - meta = self[dk].meta - data = self[dk].data - check_on = [] - for v in on_vars[:]: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars)) + items - check_on.append(items[0]) - elif meta['columns'][v].get('parent'): - msg = 'Nets can not be added to a single array item: {}' - raise ValueError(msg.format(v)) - else: - check_on.append(v) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] - - if not only_recode: - all_batches = copy.deepcopy(meta['sets']['batches']) - for n, b in all_batches.items(): - if not n in _batches: all_batches.pop(n) - languages = list(set(b['language'] for n, b in all_batches.items())) - netdef = _netdef_from_map(net_map, expand, text_prefix, languages) - if calc: - calc = _check_and_update_calc(calc, languages) - calc_only = calc.get('calc_only', False) - else: - calc_only = False - view = qp.ViewMapper() - view.make_template('frequency', {'rel_to': [None, 'y']}) - - options = {'logic': netdef, - 'axis': 'x', - 'expand': expand if expand in ['after', 'before'] else None, - 'complete': True if expand else False, - 'calc': calc, - 'calc_only': calc_only} - view.add_method('net', kwargs=options) - self.aggregate(view, False, [], _batches, on_vars, verbose=verbose) - - if recode: - ds = ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) - ds.from_stack(self, dk) - on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] - self.recode_from_net_def(ds, on_vars, net_map, expand, recode, - text_prefix, verbose) - - if checking_cluster in [None, False] or only_recode: continue - if isinstance(checking_cluster, ChainManager): - cc_keys = checking_cluster.folder_names - else: - cc_keys = checking_cluster.keys() - view['net_check'] = view.pop('net') - view['net_check']['kwargs']['iterators'].pop('rel_to') - for v in check_on: - v_net = '{}_net'.format(v) - v_net = v_net.split('.')[-1] - if not v_net in cc_keys: - checking_cluster = self._add_checking_chain(dk, checking_cluster, - v_net, v, ['@', v], ('net', ['cbase'], view)) - - return None - - @staticmethod - def _factor_labs(values, axis, rescale, drop, exclude, factor_labels, - has_factors): - if not rescale: rescale = {} - ignore = [v['value'] for v in values if v['value'] in exclude or - (not v['value'] in rescale.keys() and drop)] - if factor_labels == '()': - new_lab = '{} ({})' - split = ('(', ')') - else: - new_lab = '{} [{}]' - split = ('[', ']') - factors_mapped = {} - for v in values: - if v['value'] in ignore: continue - has_xedits = v['text'].get('x edits', {}) - has_yedits = v['text'].get('y edits', {}) - if not has_xedits: v['text']['x edits'] = {} - if not has_yedits: v['text']['y edits'] = {} - - factor = rescale[v['value']] if rescale else v['value'] - for tk, text in v['text'].items(): - if tk in ['x edits', 'y edits']: continue - for ax in axis: - try: - t = v['text']['{} edits'.format(ax)][tk] - except: - t = text - if has_factors: - fac = t.split(split[0])[-1].replace(split[1], '') - if fac == str(factor): continue - v['text']['{} edits'.format(ax)][tk] = new_lab.format(t, factor) - return values - - @staticmethod - def _add_factor_meta(dataset, var, options): - if not dataset._has_categorical_data(var): - return None - rescale = options[0] - drop = options[1] - exclude = options[2] - dataset.clear_factors(var) - all_codes = dataset.codes(var) - if rescale: - fm = rescale - else: - fm = {c: c for c in all_codes} - if not drop and rescale: - for c in all_codes: - if not c in fm: - fm[c] = c - if exclude: - for e in exclude: - if e in fm: - del fm[e] - dataset.set_factors(var, fm) - return None - - @modify(to_list=['on_vars', 'stats', 'exclude', '_batches']) - def add_stats(self, on_vars, stats=['mean'], other_source=None, rescale=None, - drop=True, exclude=None, factor_labels=True, custom_text=None, - checking_cluster=None, _batches='all', recode=False, verbose=True): - """ - Add a descriptives view to a specified collection of xks of the stack. - - Valid descriptives views: {'mean', 'stddev', 'min', 'max', 'median', 'sem'} - - Parameters - ---------- - on_vars : list - The list of x variables to add the view to. - stats : list of str, default ``['mean']`` - The metrics to compute and add as a view. - other_source : str - If provided the Link's x-axis variable will be swapped with the - (numerical) variable provided. This can be used to attach statistics - of a different variable to a Link definition. - rescale : dict - A dict that maps old to new codes, e.g. {1: 5, 2: 4, 3: 3, 4: 2, 5: 1} - drop : bool, default True - If ``rescale`` is provided all codes that are not mapped will be - ignored in the computation. - exclude : list - Codes/values to ignore in the computation. - factor_labels : bool / str, default True - Writes the (rescaled) factor values next to the category text label. - If True, square-brackets are used. If '()', normal brackets are used. - custom_text : str, default None - A custom string affix to put at the end of the requested statistics' - names. - checking_cluster : quantipy.Cluster, default None - When provided, an automated checking aggregation will be added to the - ``Cluster`` instance. - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - recode: bool, default False - Create a new variable that contains only the values - which are needed for the stat computation. The values and the included - data will be rescaled. - - Returns - ------- - None - The stack instance is modified inplace. - """ - - def _recode_from_stat_def(dataset, on_vars, rescale, drop, exclude, verbose): - for var in on_vars: - if dataset.is_array(var): continue - suffix = '_rc' - for s in [str(x) if not x == 1 else '' for x in frange('1-5')]: - suf = suffix + s - name = '{}{}'.format(var, suf) - if dataset.var_exists(name): - if dataset._meta['columns'][name]['properties'].get('recoded_stat'): - break - else: - break - if not rescale: - rescale = {x: x for x in dataset.codes(var)} - else: - rescale = copy.deepcopy(rescale) - if drop or exclude: - for x in rescale.keys(): - if not x in dataset.codes(var) or x in exclude: - rescale.pop(x) - dataset.add_meta(name, 'float', dataset.text(var)) - for x, y in rescale.items(): - sl = dataset.take({var: x}) - dataset[sl, name] = y - if verbose: - print 'Created: {}'. format(name) - dataset._meta['columns'][name]['properties'].update({'recoded_stat': var}) - return None - - def _add_factors(v, meta, values, args): - if isinstance(values, basestring): - p = values.split('@')[-1] - p_meta = meta.get('masks', meta)[p] - p_lib = meta['lib'].get('values', meta['lib']) - has_factors = p_meta.get('properties', {}).get('factor_labels', False) - v_args = args + [has_factors] - values = p_lib[p] - p_lib[p] = self._factor_labs(values, ['x', 'y'], *v_args) - if not p_meta.get('properties'): p_meta['properties'] = {} - p_meta['properties'].update({'factor_labels': True}) - else: - v_meta = meta.get('columns', meta)[v] - has_factors = v_meta.get('properties', {}).get('factor_labels') - v_args = args + [has_factors] - v_meta['values'] = self._factor_labs(values, ['x'], *v_args) - if not v_meta.get('properties'): v_meta['properties'] = {} - v_meta['properties'].update({'factor_labels': True}) - return None - - if other_source and not isinstance(other_source, str): - raise ValueError("'other_source' must be a str!") - if not rescale: drop = False - - options = {'stats': '', - 'source': other_source, - 'rescale': rescale, - 'drop': drop, 'exclude': exclude, - 'axis': 'x', - 'text': '' if not custom_text else custom_text} - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches: return None - meta = self[dk].meta - data = self[dk].data - check_on = [] - for v in on_vars: - if v in meta['sets']: - items = [i.split('@')[-1] for i in meta['sets'][v]['items']] - on_vars = list(set(on_vars + items)) - check_on = list(set(check_on + [items[0]])) - elif not meta['columns'][v].get('values'): - continue - elif not isinstance(meta['columns'][v]['values'], list): - parent = meta['columns'][v]['parent'].keys()[0].split('@')[-1] - items = [i.split('@')[-1] for i in meta['sets'][parent]['items']] - check_on = list(set(check_on + [items[0]])) - else: - check_on = list(set(check_on + [v])) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] - - ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) - ds.from_stack(self, dk) - if not other_source: - self._add_factor_meta(ds, v, (rescale, drop, exclude)) - - - view = qp.ViewMapper() - view.make_template('descriptives') - for stat in stats: - options['stats'] = stat - view.add_method('stat', kwargs=options) - self.aggregate(view, False, on_vars, _batches, on_vars, verbose=verbose) - - if recode: - if other_source: - raise ValueError('Cannot recode if other_source is provided.') - ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) - ds.from_stack(self, dk) - on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] - _recode_from_stat_def(ds, on_vars, rescale, drop, exclude, verbose) - - if factor_labels: - args = [rescale, drop, exclude, factor_labels] - all_batches = meta['sets']['batches'].keys() - if not _batches: _batches = all_batches - batches = [b for b in all_batches if b in _batches] - for v in check_on: - globally = False - for b in batches: - batch_me = meta['sets']['batches'][b]['meta_edits'] - values = batch_me.get(v, {}).get('values', []) - if not values: - globally = True - else: - _add_factors(v, batch_me, values, args) - if globally: - values = meta['columns'][v]['values'] - _add_factors(v, meta, values, args) - if isinstance(checking_cluster, ChainManager): - cm_meta = checking_cluster.stack['checks'].meta - values = cm_meta['columns'][v]['values'] - _add_factors(v, cm_meta, values, args) - if checking_cluster and 'mean' in stats and check_on: - - options['stats'] = 'mean' - c_view = qp.ViewMapper().make_template('descriptives') - c_view.add_method('stat_check', kwargs=options) - - views = ('stat', ['cbase', 'counts'], c_view) - checking_cluster = self._add_checking_chain(dk, checking_cluster, - 'stat_check', check_on, ['@'], views) - - return None - - @modify(to_list=['_batches']) - def add_tests(self, _batches='all', verbose=True): - """ - Apply coltests for selected batches. - - Sig. Levels are taken from ``qp.Batch`` definitions. - - Parameters - ---------- - _batches: str or list of str - Only for ``qp.Links`` that are defined in this ``qp.Batch`` - instances views are added. - - Returns - ------- - None - """ - self._remove_coltests() - - if verbose: - start = time.time() - - for dk in self.keys(): - _batches = self._check_batches(dk, _batches) - if not _batches: return None - for batch_name in _batches: - batch = self[dk].meta['sets']['batches'][batch_name] - sigpro = batch.get('sigproperties', {}) - levels = batch.get('sigproperties', batch).get('siglevels', []) - weight = batch['weights'] - x_y = batch['x_y_map'] - x_f = batch['x_filter_map'] - y_f = batch['y_filter_map'] - yks = batch['yks'] - - if levels: - vm_tests = qp.ViewMapper().make_template( - method='coltests', - iterators={'metric': ['props', 'means'], - 'mimic': sigpro.get('mimic', ['Dim']), - 'level': levels}) - vm_tests.add_method('significance', - kwargs = {'flag_bases': sigpro.get('flag_bases', [30, 100]), - 'test_total': sigpro.get('test_total', None), - 'groups': 'Tests'}) - for yy in batch['y_on_y']: - if y_f[yy]: - fy = y_f[yy].encode('utf8') - f = {fy: {fy: 0}} - else: - f = ['no_filter'] - self.add_link(filters=f, x=yks[1:], y=yks, - views=vm_tests, weights=weight) - total_len = len(x_y) - for idx, xy in enumerate(x_y, 1): - x, y = xy - if x == '@': continue - if x_f[x]: - fx = x_f[x].encode('utf8') - f = {fx: {fx: 0}} - else: - f = ['no_filter'] - self.add_link(filters=f, x=x, y=y, - views=vm_tests, weights=weight) - if verbose: - done = float(idx) / float(total_len) *100 - print '\r', - time.sleep(0.01) - print 'Batch [{}]: {} %'.format(batch_name, round(done, 1)), - sys.stdout.flush() - if verbose and levels: print '\n' - if verbose: print 'Sig-Tests:', time.time()-start - return None - - def _remove_coltests(self, props=True, means=True): - """ - Remove coltests from stack. - - Parameters - ---------- - props : bool, default=True - If True, column proportion test view will be removed from stack. - means : bool, default=True - If True, column mean test view will be removed from stack. - """ - for dk in self.keys(): - for fk in self[dk].keys(): - for xk in self[dk][fk].keys(): - for yk in self[dk][fk][xk].keys(): - for vk in self[dk][fk][xk][yk].keys(): - del_prop = props and 't.props' in vk - del_mean = means and 't.means' in vk - if del_prop or del_mean: - del self[dk][fk][xk][yk][vk] - del self[dk][fk][xk][yk][vk] - return None + +#-*- coding: utf-8 -*- +import io +import itertools +import json +import pandas as pd +import numpy as np +import quantipy as qp +import copy +import time +import sys +import warnings + +from link import Link +from chain import Chain +from view import View +from helpers import functions +from view_generators.view_mapper import ViewMapper +from view_generators.view_maps import QuantipyViews +from quantipy.core.tools.qp_decorators import modify +from quantipy.core.tools.dp.spss.reader import parse_sav_file +from quantipy.core.tools.dp.io import unicoder, write_quantipy +from quantipy.core.tools.dp.prep import frequency, verify_test_results, frange +from quantipy.core.tools.view.logic import ( + has_any, has_all, has_count, + not_any, not_all, not_count, + is_lt, is_ne, is_gt, + is_le, is_eq, is_ge, + union, intersection, get_logic_index) +from cache import Cache + +import itertools +from collections import defaultdict, OrderedDict + +# Pickle modules +import cPickle + +# Compression methods +import gzip + +from quantipy.sandbox.sandbox import Chain as NewChain +from quantipy.sandbox.sandbox import ChainManager + + +class Stack(defaultdict): + """ + Container of quantipy.Link objects holding View objects. + + A Stack is nested dictionary that structures the data and variable + relationships storing all View aggregations performed. + """ + + def __init__(self, + name="", + add_data=None): + + super(Stack, self).__init__(Stack) + + self.name = name + self.key = None + self.parent = None + + # This is the root of the stack + # It is used by the get/set methods to determine + # WHERE in the stack those methods are. + self.stack_pos = "stack_root" + + self.x_variables = None + self.y_variables = None + + self.__view_keys = [] + + if add_data: + for key in add_data: + if isinstance(add_data[key], dict): + self.add_data( + data_key=key, + data=add_data[key].get('data', None), + meta=add_data[key].get('meta', None) + ) + elif isinstance(add_data[key], tuple): + self.add_data( + data_key=key, + data=add_data[key][0], + meta=add_data[key][1] + ) + else: + raise TypeError( + "All data_key values must be one of the following types: " + " or . " + "Given: %s" % (type(add_data[key])) + ) + + def __setstate__(self, attr_dict): + self.__dict__.update(attr_dict) + + def __reduce__(self): + arguments = (self.name, ) + state = self.__dict__.copy() + if 'cache' in state: + state.pop('cache') + state['cache'] = Cache() # Empty the cache for storage + return self.__class__, arguments, state, None, self.iteritems() + + def __setitem__(self, key, val): + """ The 'set' method for the Stack(dict) + + It 'sets' the value in it's correct place in the Stack + AND applies a 'stack_pos' value depending on WHERE in + the stack the value is being placed. + """ + super(Stack, self).__setitem__(key, val) + + # The 'meta' portion of the stack is a standar dict (not Stack) + try: + if isinstance(val, Stack) and val.stack_pos is "stack_root": + val.parent = self + val.key = key + + # This needs to be compacted and simplified. + if self.stack_pos is "stack_root": + val.stack_pos = "data_root" + elif self.stack_pos is "data_root": + val.stack_pos = "filter" + elif self.stack_pos is "filter": + val.stack_pos = "x" + + except AttributeError: + pass + + def __getitem__(self, key): + """ The 'get' method for the Stack(dict) + + The method 'gets' a value from the stack. If 'stack_pos' is 'y' + AND the value isn't a Link instance THEN it tries to query the + stack again with the x/y variables swapped and IF that yelds + a result that is a Link object THEN it sets a 'transpose' variable + as True in the result and the result is transposed. + """ + val = defaultdict.__getitem__(self, key) + return val + + def add_data(self, data_key, data=None, meta=None, ): + """ + Sets the data_key into the stack, optionally mapping data sources it. + + It is possible to handle the mapping of data sources in different ways: + + * no meta or data (for proxy links not connected to source data) + * meta only (for proxy links with supporintg meta) + * data only (meta will be inferred if possible) + * data and meta + + Parameters + ---------- + data_key : str + The reference name for a data source connected to the Stack. + data : pandas.DataFrame + The input (case) data source. + meta : dict or OrderedDict + A quantipy compatible metadata source that describes the case data. + + Returns + ------- + None + """ + self._verify_key_types(name='data', keys=data_key) + + if data_key in self.keys(): + warning_msg = "You have overwritten data/meta for key: ['%s']." + print warning_msg % (data_key) + + if data is not None: + if isinstance(data, pd.DataFrame): + if meta is None: + # To do: infer meta from DataFrame + meta = {'info': None, 'lib': None, 'sets': None, + 'columns': None, 'masks': None} + # Add a special column of 1s + data['@1'] = np.ones(len(data.index)) + data.index = list(xrange(0, len(data.index))) + else: + raise TypeError( + "The 'data' given to Stack.add_data() must be one of the following types: " + "" + ) + + if not meta is None: + if isinstance(meta, (dict, OrderedDict)): + # To do: verify incoming meta + pass + else: + raise TypeError( + "The 'meta' given to Stack.add_data() must be one of the following types: " + ", ." + ) + + # Add the data key to the stack + # self[data_key] = {} + + # Add the meta and data to the data_key position in the stack + self[data_key].meta = meta + self[data_key].data = data + self[data_key].cache = Cache() + self[data_key]['no_filter'].data = self[data_key].data + + def remove_data(self, data_keys): + """ + Deletes the data_key(s) and associated data specified in the Stack. + + Parameters + ---------- + data_keys : str or list of str + The data keys to remove. + + Returns + ------- + None + """ + self._verify_key_types(name='data', keys=data_keys) + if isinstance(data_keys, (str, unicode)): + data_keys = [data_keys] + for data_key in data_keys: + del self[data_key] + + def variable_types(self, data_key, only_type=None, verbose=True): + """ + Group variables by data types found in the meta. + + Parameters + ---------- + data_key : str + The reference name of a case data source hold by the Stack instance. + only_type : {'int', 'float', 'single', 'delimited set', 'string', + 'date', time', 'array'}, optional + Will restrict the output to the given data type. + + Returns + ------- + types : dict or list of str + A summary of variable names mapped to their data types, in form of + {type_name: [variable names]} or a list of variable names + confirming only_type. + """ + if self[data_key].meta['columns'] is None: + return 'No meta attached to data_key: %s' %(data_key) + else: + types = { + 'int': [], + 'float': [], + 'single': [], + 'delimited set': [], + 'string': [], + 'date': [], + 'time': [], + 'array': [] + } + not_found = [] + for col in self[data_key].data.columns: + if not col in ['@1', 'id_L1', 'id_L1.1']: + try: + types[ + self[data_key].meta['columns'][col]['type'] + ].append(col) + except: + not_found.append(col) + for mask in self[data_key].meta['masks'].keys(): + types[self[data_key].meta['masks'][mask]['type']].append(mask) + if not_found and verbose: + print '%s not found in meta file. Ignored.' %(not_found) + if only_type: + return types[only_type] + else: + return types + + def apply_meta_edits(self, batch_name, data_key, filter_key=None, + freeze=False): + """ + Take over meta_edits from Batch definitions. + + Parameters + ---------- + batch_name: str + Name of the Batch whose meta_edits are taken. + data_key: str + Accessing this metadata: ``self[data_key].meta`` + Batch definitions are takes from here and this metadata is modified. + filter_key: str, default None + Currently not implemented! + Accessing this metadata: ``self[data_key][filter_key].meta`` + Batch definitions are takes from here and this metadata is modified. + """ + if filter_key: + raise NotImplementedError("'filter_key' is not implemented.") + if freeze: + self.freeze_master_meta(data_key) + meta = self[data_key].meta + batch = meta['sets']['batches'][batch_name] + for name, e_meta in batch['meta_edits'].items(): + if name == 'lib': + continue + elif name in meta['masks']: + meta['masks'][name] = e_meta + try: + lib = batch['meta_edits']['lib'][name] + meta['lib']['values'][name] = lib + except: + pass + else: + meta['columns'][name] = e_meta + meta['lib']['default text'] = batch['language'] + return None + + def freeze_master_meta(self, data_key, filter_key=None): + """ + Save ``.meta`` in ``.master_meta`` for a defined data_key. + + Parameters + ---------- + data_key: str + Using: ``self[data_key]`` + filter_key: str, default None + Currently not implemented! + Using: ``self[data_key][filter_key]`` + """ + if filter_key: + raise NotImplementedError("'filter_key' is not implemented.") + self[data_key].master_meta = copy.deepcopy(self[data_key].meta) + self[data_key].meta = copy.deepcopy(self[data_key].meta) + return None + + def restore_meta(self, data_key, filter_key=None): + """ + Restore the ``.master_meta`` for a defined data_key if it exists. + + Undo self.apply_meta_edits() + + Parameters + ---------- + data_key: str + Accessing this metadata: ``self[data_key].meta`` + filter_key: str, default None + Currently not implemented! + Accessing this metadata: ``self[data_key][filter_key].meta`` + """ + if filter_key: + raise NotImplementedError("'filter_key' is not implemented.") + try: + self[data_key].meta = copy.deepcopy(self[data_key].master_meta) + except: + pass + return None + + def get_chain(self, *args, **kwargs): + + if qp.OPTIONS['new_chains']: + chain = ChainManager(self) + chain = chain.get(*args, **kwargs) + return chain + else: + def _get_chain(name=None, data_keys=None, filters=None, x=None, y=None, + views=None, orient_on=None, select=None, + rules=False, rules_weight=None, described=None): + """ + Construct a "chain" shaped subset of Links and their Views from the Stack. + + A chain is a one-to-one or one-to-many relation with an orientation that + defines from which axis (x or y) it is build. + + Parameters + ---------- + name : str, optional + If not provided the name of the chain is generated automatically. + data_keys, filters, x, y, views : str or list of str + Views will be added reflecting the order in ``views`` parameter. If + both ``x`` and ``y`` have multiple items, you must specify the + ``orient_on`` parameter. + orient_on : {'x', 'y'}, optional + Must be specified if both ``x`` and ``y`` are lists of multiple + items. + select : tbc. + :TODO: document this! + + Returns + ------- + chain : Chain object instance + """ + + #Make sure all the given keys are in lists + data_keys = self._force_key_as_list(data_keys) + # filters = self._force_key_as_list(filters) + views = self._force_key_as_list(views) + + #Make sure all the given keys are in lists + x = self._force_key_as_list(x) + y = self._force_key_as_list(y) + + if orient_on is None: + if len(x)==1: + orientation = 'x' + elif len(y)==1: + orientation = 'y' + else: + orientation = 'x' + else: + orientation = orient_on + if described is None: + described = self.describe() + + if isinstance(rules, bool): + if rules: + rules = ['x', 'y'] + else: + rules = [] + + if orient_on: + if x is None: + x = described['x'].drop_duplicates().values.tolist() + if y is None: + y = described['y'].drop_duplicates().values.tolist() + if views is None: + views = self._Stack__view_keys + views = [v for v in views if '|default|' not in v] + chains = self.__get_chains( + name=name, + data_keys=data_keys, + filters=filters, + x=x, + y=y, + views=views, + orientation=orient_on, + select=select, + rules=rules, + rules_weight=rules_weight) + return chains + else: + chain = Chain(name) + found_views = [] + + #Make sure all the given keys are in lists + x = self._force_key_as_list(x) + y = self._force_key_as_list(y) + + if data_keys is None: + # Apply lazy data_keys if none given + data_keys = self.keys() + + the_filter = "no_filter" if filters is None else filters + + if self.__has_list(data_keys): + for key in data_keys: + # Use describe method to get x keys if not supplied. + if x is None: + x_keys = described['x'].drop_duplicates().values.tolist() + else: + x_keys = x + + # Use describe method to get y keys if not supplied. + if y is None: + y_keys = described['y'].drop_duplicates().values.tolist() + else: + y_keys = y + + # Use describe method to get view keys if not supplied. + if views is None: + v_keys = described['view'].drop_duplicates().values.tolist() + v_keys = [v_key for v_key in v_keys if '|default|' + not in v_key] + else: + v_keys = views + + chain._derive_attributes( + key, the_filter, x_keys, y_keys, views, orientation=orientation) + + # Apply lazy name if none given + if name is None: + chain._lazy_name() + + for x_key in x_keys: + self._verify_key_exists( + x_key, + stack_path=[key, the_filter] + ) + + for y_key in y_keys: + self._verify_key_exists( + y_key, + stack_path=[key, the_filter, x_key]) + + + try: + base_text = self[key].meta['columns'][x_key]['properties']['base_text'] + if isinstance(base_text, (str, unicode)): + if base_text.startswith(('Base:', 'Bas:')): + base_text = base_text.split(':')[-1].lstrip() + elif isinstance(base_text, dict): + for text_key in base_text.keys(): + if base_text[text_key].startswith(('Base:', 'Bas:')): + base_text[text_key] = base_text[text_key].split(':')[-1].lstrip() + chain.base_text = base_text + except: + pass + if views is None: + chain[key][the_filter][x_key][y_key] = self[key][the_filter][x_key][y_key] + else: + stack_link = self[key][the_filter][x_key][y_key] + link_keys = stack_link.keys() + chain_link = {} + chain_view_keys = [k for k in views if k in link_keys] + for vk in chain_view_keys: + stack_view = stack_link[vk] + # Get view dataframe + rules_x_slicer = self.axis_slicer_from_vartype( + rules, 'x', key, the_filter, x_key, y_key, rules_weight) + + rules_y_slicer = self.axis_slicer_from_vartype( + rules, 'y', key, the_filter, x_key, y_key, rules_weight) + if rules_x_slicer is None and rules_y_slicer is None: + # No rules to apply + view_df = stack_view.dataframe + else: + # Apply rules + viable_axes = functions.rule_viable_axes(self[key].meta, vk, x_key, y_key) + transposed_array_sum = x_key == '@' and y_key in self[key].meta['masks'] + if not viable_axes: + # Axes are not viable for rules application + view_df = stack_view.dataframe + else: + view_df = stack_view.dataframe.copy() + if 'x' in viable_axes and not rules_x_slicer is None: + # Apply x-rules + rule_codes = set(rules_x_slicer) + view_codes = set(view_df.index.tolist()) + if not rule_codes - view_codes: + view_df = view_df.loc[rules_x_slicer] + if 'x' in viable_axes and transposed_array_sum and rules_y_slicer: + view_df = view_df.loc[rules_y_slicer] + if 'y' in viable_axes and not rules_y_slicer is None: + # Apply y-rules + view_df = view_df[rules_y_slicer] + if vk.split('|')[1].startswith('t.'): + view_df = verify_test_results(view_df) + chain_view = View( + link=stack_link, + name = stack_view.name, + kwargs=stack_view._kwargs) + chain_view._notation = vk + chain_view.grp_text_map = stack_view.grp_text_map + chain_view.dataframe = view_df + chain_view._custom_txt = stack_view._custom_txt + chain_view.add_base_text = stack_view.add_base_text + chain_link[vk] = chain_view + if vk not in found_views: + found_views.append(vk) + + chain[key][the_filter][x_key][y_key] = chain_link + else: + raise ValueError( + "One or more of your data_keys ({data_keys}) is not" + " in the stack ({stack_keys})".format( + data_keys=data_keys, + stack_keys=self.keys() + ) + ) + + # Make sure chain.views only contains views that actually exist + # in the chain + if found_views: + chain.views = [ + view + for view in chain.views + if view in found_views] + return chain + + return _get_chain(*args, **kwargs) + + def reduce(self, data_keys=None, filters=None, x=None, y=None, variables=None, views=None): + ''' + Remove keys from the matching levels, erasing discrete Stack portions. + + Parameters + ---------- + data_keys, filters, x, y, views : str or list of str + + Returns + ------- + None + ''' + + # Ensure given keys are all valid types + self._verify_multiple_key_types( + data_keys=data_keys, + filters=filters, + x=x, + y=y, + variables=variables, + views=views + ) + + # Make sure all the given keys are in lists + data_keys = self._force_key_as_list(data_keys) + filters = self._force_key_as_list(filters) + views = self._force_key_as_list(views) + if not variables is None: + variables = self._force_key_as_list(variables) + x = variables + y = variables + else: + x = self._force_key_as_list(x) + y = self._force_key_as_list(y) + + # Make sure no keys that don't exist anywhere were passed + key_check = { + 'data': data_keys, + 'filter': filters, + 'x': x, + 'y': y, + 'view': views + } + + contents = self.describe() + for key_type, keys in key_check.iteritems(): + if not keys is None: + uk = contents[key_type].unique() + if not any([tk in uk for tk in keys]): + raise ValueError( + "Some of the %s keys passed to stack.reduce() " + "weren't found. Found: %s. " + "Given: %s" % (key_type, uk, keys) + ) + + if not data_keys is None: + for dk in data_keys: + try: + del self[dk] + except: + pass + + for dk in self.keys(): + if not filters is None: + for fk in filters: + try: + del self[dk][fk] + except: + pass + + for fk in self[dk].keys(): + if not x is None: + for xk in x: + try: + del self[dk][fk][xk] + except: + pass + + for xk in self[dk][fk].keys(): + if not y is None: + for yk in y: + try: + del self[dk][fk][xk][yk] + except: + pass + + for yk in self[dk][fk][xk].keys(): + if not views is None: + for vk in views: + try: + del self[dk][fk][xk][yk][vk] + except: + pass + + def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, + views=None, weights=None, variables=None): + """ + Add Link and View defintions to the Stack. + + The method can be used flexibly: It is possible to pass only Link + defintions that might be composed of filter, x and y specifications, + only views incl. weight variable selections or arbitrary combinations of + the former. + + :TODO: Remove ``variables`` from parameter list and method calls. + + Parameters + ---------- + data_keys : str, optional + The data_key to be added to. If none is given, the method will try + to add to all data_keys found in the Stack. + filters : list of str describing filter defintions, default ['no_filter'] + The string must be a valid input for the + pandas.DataFrame.query() method. + x, y : str or list of str + The x and y variables to constrcut Links from. + views : list of view method names. + Can be any of Quantipy's preset Views or the names of created + view method specifications. + weights : list, optional + The names of weight variables to consider in the data aggregation + process. Weight variables must be of type ``float``. + + Returns + ------- + None + """ + if data_keys is None: + data_keys = self.keys() + else: + self._verify_key_types(name='data', keys=data_keys) + data_keys = self._force_key_as_list(data_keys) + + if not isinstance(views, ViewMapper): + # Use DefaultViews if no view were given + if views is None: + pass + # views = DefaultViews() + elif isinstance(views, (list, tuple)): + views = QuantipyViews(views=views) + else: + raise TypeError( + "The views past to stack.add_link() must be type , " + "or they must be a list of method names known to ." + ) + + qplogic_filter = False + if not isinstance(filters, dict): + self._verify_key_types(name='filter', keys=filters) + filters = self._force_key_as_list(filters) + filters = {f: f for f in filters} + # if filters.keys()[0] != 'no_filter': + # msg = ("Warning: pandas-based filtering will be deprecated in the " + # "future!\nPlease switch to quantipy-logic expressions.") + # print UserWarning(msg) + else: + qplogic_filter = True + + if not variables is None: + if not x is None or not y is None: + raise ValueError( + "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " + "at the same time." + ) + + x = self._force_key_as_list(x) + y = self._force_key_as_list(y) + + # Get the lazy y keys none were given and there is only 1 x key + if not x is None: + if len(x)==1 and y is None: + y = self.describe( + index=['y'], + query="x=='%s'" % (x[0]) + ).index.tolist() + + # Get the lazy x keys none were given and there is only 1 y key + if not y is None: + if len(y)==1 and x is None: + x = self.describe( + index=['x'], + query="y=='%s'" % (y[0]) + ).index.tolist() + + for dk in data_keys: + self._verify_key_exists(dk) + for filter_def, logic in filters.items(): + # qp.OPTIONS-based hack to allow faster stack filters + # ------------------------------------------------------ + if qp.OPTIONS['fast_stack_filters']: + if not filter_def in self[dk].keys(): + if filter_def=='no_filter': + self[dk][filter_def].data = self[dk].data + self[dk][filter_def].meta = self[dk].meta + else: + if not qplogic_filter: + try: + self[dk][filter_def].data = self[dk].data.query(logic) + self[dk][filter_def].meta = self[dk].meta + except Exception, ex: + raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) + continue + else: + dataset = qp.DataSet('stack') + dataset.from_components(self[dk].data, self[dk].meta) + f_dataset = dataset.filter(filter_def, logic, inplace=False) + self[dk][filter_def].data = f_dataset._data + self[dk][filter_def].meta = f_dataset._meta + else: + if filter_def=='no_filter': + self[dk][filter_def].data = self[dk].data + self[dk][filter_def].meta = self[dk].meta + else: + if not qplogic_filter: + try: + self[dk][filter_def].data = self[dk].data.query(logic) + self[dk][filter_def].meta = self[dk].meta + except Exception, ex: + raise UserWarning('A filter definition is invalid and will be skipped: {filter_def}'.format(filter_def=filter_def)) + continue + else: + dataset = qp.DataSet('stack') + dataset.from_components(self[dk].data, self[dk].meta) + f_dataset = dataset.filter(filter_def, logic, inplace=False) + self[dk][filter_def].data = f_dataset._data + self[dk][filter_def].meta = f_dataset._meta + fdata = self[dk][filter_def].data + + if len(fdata) == 0: + raise UserWarning('A filter definition resulted in no cases and will be skipped: {filter_def}'.format(filter_def=filter_def)) + continue + self.__create_links(data=fdata, data_key=dk, the_filter=filter_def, x=x, y=y, views=views, weights=weights, variables=variables) + + def describe(self, index=None, columns=None, query=None, split_view_names=False): + """ + Generates a structured overview of all Link defining Stack elements. + + Parameters + ---------- + index, columns : str of or list of {'data', 'filter', 'x', 'y', 'view'}, + optional + Controls the output representation by structuring a pivot-style + table according to the index and column values. + query : str + A query string that is valid for the pandas.DataFrame.query() method. + split_view_names : bool, default False + If True, will create an output of unique view name notations split + up into their components. + + Returns + ------- + description : pandas.DataFrame + DataFrame summing the Stack's structure in terms of Links and Views. + """ + stack_tree = [] + for dk in self.keys(): + path_dk = [dk] + filters = self[dk] + +# for fk in filters.keys(): +# path_fk = path_dk + [fk] +# xs = self[dk][fk] + + for fk in filters.keys(): + path_fk = path_dk + [fk] + xs = self[dk][fk] + + for sk in xs.keys(): + path_sk = path_fk + [sk] + ys = self[dk][fk][sk] + + for tk in ys.keys(): + path_tk = path_sk + [tk] + views = self[dk][fk][sk][tk] + + if views.keys(): + for vk in views.keys(): + path_vk = path_tk + [vk, 1] + stack_tree.append(tuple(path_vk)) + else: + path_vk = path_tk + ['|||||', 1] + stack_tree.append(tuple(path_vk)) + + column_names = ['data', 'filter', 'x', 'y', 'view', '#'] + description = pd.DataFrame.from_records(stack_tree, columns=column_names) + if split_view_names: + views_as_series = pd.DataFrame( + description.pivot_table(values='#', columns='view', aggfunc='count') + ).reset_index()['view'] + parts = ['xpos', 'agg', 'condition', 'rel_to', 'weights', + 'shortname'] + description = pd.concat( + (views_as_series, + pd.DataFrame(views_as_series.str.split('|').tolist(), + columns=parts)), axis=1) + + description.replace('|||||', np.NaN, inplace=True) + if query is not None: + description = description.query(query) + if not index is None or not columns is None: + description = description.pivot_table(values='#', index=index, columns=columns, + aggfunc='count') + return description + + def refresh(self, data_key, new_data_key='', new_weight=None, + new_data=None, new_meta=None): + """ + Re-run all or a portion of Stack's aggregations for a given data key. + + refresh() can be used to re-weight the data using a new case data + weight variable or to re-run all aggregations based on a changed source + data version (e.g. after cleaning the file/ dropping cases) or a + combination of the both. + + .. note:: + Currently this is only supported for the preset QuantipyViews(), + namely: ``'cbase'``, ``'rbase'``, ``'counts'``, ``'c%'``, + ``'r%'``, ``'mean'``, ``'ebase'``. + + Parameters + ---------- + data_key : str + The Links' data key to be modified. + new_data_key : str, default '' + Controls if the existing data key's files and aggregations will be + overwritten or stored via a new data key. + new_weight : str + The name of a new weight variable used to re-aggregate the Links. + new_data : pandas.DataFrame + The case data source. If None is given, the + original case data found for the data key will be used. + new_meta : quantipy meta document + A meta data source associated with the case data. If None is given, + the original meta definition found for the data key will be used. + + Returns + ------- + None + """ + content = self.describe()[['data', 'filter', 'x', 'y', 'view']] + content = content[content['data'] == data_key] + put_meta = self[data_key].meta if new_meta is None else new_meta + put_data = self[data_key].data if new_data is None else new_data + dk = new_data_key if new_data_key else data_key + self.add_data(data_key=dk, data=put_data, meta=put_meta) + skipped_views = [] + for _, f, x, y, view in content.values: + shortname = view.split('|')[-1] + if shortname not in ['default', 'cbase', 'cbase_gross', + 'rbase', 'counts', 'c%', + 'r%', 'ebase', 'mean', + 'c%_sum', 'counts_sum']: + if view not in skipped_views: + skipped_views.append(view) + warning_msg = ('\nOnly preset QuantipyViews are supported.' + 'Skipping: {}').format(view) + print warning_msg + else: + view_weight = view.split('|')[-2] + if not x in [view_weight, new_weight]: + if new_data is None and new_weight is not None: + if not view_weight == '': + if new_weight == '': + weight = [None, view_weight] + else: + weight = [view_weight, new_weight] + else: + if new_weight == '': + weight = None + else: + weight = [None, new_weight] + self.add_link(data_keys=dk, filters=f, x=x, y=y, + weights=weight, views=[shortname]) + else: + if view_weight == '': + weight = None + elif new_weight is not None: + if not (view_weight == new_weight): + if new_weight == '': + weight = [None, view_weight] + else: + weight = [view_weight, new_weight] + else: + weight = view_weight + else: + weight = view_weight + try: + self.add_link(data_keys=dk, filters=f, x=x, y=y, + weights=weight, views=[shortname]) + except ValueError, e: + print '\n', e + return None + + def save(self, path_stack, compression="gzip", store_cache=True, + decode_str=False, dataset=False, describe=False): + """ + Save Stack instance to .stack file. + + Parameters + ---------- + path_stack : str + The full path to the .stack file that should be created, including + the extension. + compression : {'gzip'}, default 'gzip' + The intended compression type. + store_cache : bool, default True + Stores the MatrixCache in a file in the same location. + decode_str : bool, default=True + If True the unicoder function will be used to decode all str + objects found anywhere in the meta document/s. + dataset : bool, default=False + If True a json/csv will be saved parallel to the saved stack + for each data key in the stack. + describe : bool, default=False + If True the result of stack.describe().to_excel() will be + saved parallel to the saved stack. + + Returns + ------- + None + """ + protocol = cPickle.HIGHEST_PROTOCOL + if not path_stack.endswith('.stack'): + raise ValueError( + "To avoid ambiguity, when using Stack.save() you must provide the full path to " + "the stack file you want to create, including the file extension. For example: " + "stack.save(path_stack='./output/MyStack.stack'). Your call looks like this: " + "stack.save(path_stack='%s', ...)" % (path_stack) + ) + + # Make sure there are no str objects in any meta documents. If + # there are any non-ASCII characters will be encoded + # incorrectly and lead to UnicodeDecodeErrors in Jupyter. + if decode_str: + for dk in self.keys(): + self[dk].meta = unicoder(self[dk].meta) + + if compression is None: + f = open(path_stack, 'wb') + cPickle.dump(self, f, protocol) + else: + f = gzip.open(path_stack, 'wb') + cPickle.dump(self, f, protocol) + + if store_cache: + caches = {} + for key in self.keys(): + caches[key] = self[key].cache + + path_cache = path_stack.replace('.stack', '.cache') + if compression is None: + f1 = open(path_cache, 'wb') + cPickle.dump(caches, f1, protocol) + else: + f1 = gzip.open(path_cache, 'wb') + cPickle.dump(caches, f1, protocol) + + f1.close() + + f.close() + + if dataset: + for key in self.keys(): + path_json = path_stack.replace( + '.stack', + ' [{}].json'.format(key)) + path_csv = path_stack.replace( + '.stack', + ' [{}].csv'.format(key)) + write_quantipy( + meta=self[key].meta, + data=self[key].data, + path_json=path_json, + path_csv=path_csv) + + if describe: + path_describe = path_stack.replace('.stack', '.xlsx') + self.describe().to_excel(path_describe) + + # def get_slice(data_key=None, x=None, y=None, filters=None, views=None): + # """ """ + # pass + + # STATIC METHODS + + @staticmethod + def from_sav(data_key, filename, name=None, path=None, ioLocale="en_US.UTF-8", ioUtf8=True): + """ + Creates a new stack instance from a .sav file. + + Parameters + ---------- + data_key : str + The data_key for the data and meta in the sav file. + filename : str + The name to the sav file. + name : str + A name for the sav (stored in the meta). + path : str + The path to the sav file. + ioLocale : str + The locale used in during the sav processing. + ioUtf8 : bool + Boolean that indicates the mode in which text communicated to or + from the I/O module will be. + + Returns + ------- + stack : stack object instance + A stack instance that has a data_key with data and metadata + to run aggregations. + """ + if name is None: + name = data_key + + meta, data = parse_sav_file(filename=filename, path=path, name=name, ioLocale=ioLocale, ioUtf8=ioUtf8) + return Stack(add_data={name: {'meta': meta, 'data':data}}) + + @staticmethod + def load(path_stack, compression="gzip", load_cache=False): + """ + Load Stack instance from .stack file. + + Parameters + ---------- + path_stack : str + The full path to the .stack file that should be created, including + the extension. + compression : {'gzip'}, default 'gzip' + The compression type that has been used saving the file. + load_cache : bool, default False + Loads MatrixCache into the Stack a .cache file is found. + + Returns + ------- + None + """ + + + if not path_stack.endswith('.stack'): + raise ValueError( + "To avoid ambiguity, when using Stack.load() you must provide the full path to " + "the stack file you want to create, including the file extension. For example: " + "stack.load(path_stack='./output/MyStack.stack'). Your call looks like this: " + "stack.load(path_stack='%s', ...)" % (path_stack) + ) + + if compression is None: + f = open(path_stack, 'rb') + else: + f = gzip.open(path_stack, 'rb') + new_stack = cPickle.load(f) + f.close() + + if load_cache: + path_cache = path_stack.replace('.stack', '.cache') + if compression is None: + f = open(path_cache, 'rb') + else: + f = gzip.open(path_cache, 'rb') + caches = cPickle.load(f) + for key in caches.keys(): + if key in new_stack.keys(): + new_stack[key].cache = caches[key] + else: + raise ValueError( + "Tried to insert a loaded MatrixCache in to a data_key in the stack that" + "is not in the stack. The data_key is '{}', available keys are {}" + .format(key, caches.keys()) + ) + f.close() + + return new_stack + + + # PRIVATE METHODS + + def __get_all_y_keys(self, data_key, the_filter="no_filter"): + if(self.stack_pos == 'stack_root'): + return self[data_key].y_variables + else: + raise KeyError("get_all_y_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) + + def __get_all_x_keys(self, data_key, the_filter="no_filter"): + if(self.stack_pos == 'stack_root'): + return self[data_key].x_variables + else: + raise KeyError("get_all_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) + + def __get_all_x_keys_except(self, data_key, exception): + keys = self.__get_all_x_keys(data_key) + return [i for i in keys if i != exception[0]] + + def __get_all_y_keys_except(self, data_key, exception): + keys = self.__get_all_y_keys(data_key) + return [i for i in keys if i != exception[0]] + + def __set_x_key(self, key): + if self.x_variables is None: + self.x_variables = set(key) + else: + self.x_variables.update(key) + + def __set_y_key(self, key): + if self.y_variables is None: + self.y_variables = set(key) + else: + self.y_variables.update(key) + + def _set_x_and_y_keys(self, data_key, x, y): + """ + Sets the x_variables and y_variables in the data part of the stack for this data_key, e.g. stack['Jan']. + This method can also be used to add to the current lists and it makes sure the list stays unique. + """ + if self.stack_pos == 'stack_root': + self[data_key].__set_x_key(x) + self[data_key].__set_y_key(y) + else: + raise KeyError("set_x_keys can only be called from a stack at root level. Current level is '{0}'".format(self.stack_pos)) + + def __create_combinations(self, data, data_key, x=None, y=None, weight=None, variables=None): + if isinstance(y, str): + y = [y] + if isinstance(x, str): + x = [x] + + has_metadata = self[data_key].meta is not None and not isinstance(self[data_key].meta, Stack) + + # any(...) returns true if ANY of the vars are not None + if any([x, y]) and variables is not None: + # Raise an error if variables AND x/y are BOTH supplied + raise ValueError("Either use the 'variables' OR 'x', 'y' NOT both.") + + if not any([x, y]): + if variables is None: + if not has_metadata: + # "fully-lazy" method. (variables, x and y are all None) + variables = data.columns.tolist() + + if variables is not None: + x = variables + y = variables + variables = None + + # Ensure that we actually have metadata + if has_metadata: + # THEN we try to create the combinations with metadata + combinations = self.__create_combinations_with_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) + else: + # Either variables or both x AND y are supplied. Then create the combinations from that. + combinations = self.__create_combinations_no_meta(data=data, data_key=data_key, x=x, y=y, weight=weight) + + unique_list = set([item for comb in combinations for item in comb]) + + return combinations, unique_list + + def __create_combinations_with_meta(self, data, data_key, x=None, y=None, weight=None): + # TODO: These meta functions should possibly be in the helpers functions + metadata_columns = self[data_key].meta['columns'].keys() + for mask, mask_data in self[data_key].meta['masks'].iteritems(): + # TODO :: Get the static list from somewhere. not hardcoded. + if mask_data['type'].lower() in ['array', 'dichotomous set', + "categorical set"]: + metadata_columns.append(mask) + for item in mask_data['items']: + if "source" in item: + column = item["source"].split('@')[1] + metadata_columns.remove(column) + elif mask_data['type'].lower() in ["overlay"]: + pass + # Use all from the metadata, if nothing is specified (fully-lazy) + if x is None and y is None: + x = metadata_columns + y = metadata_columns + if all([x, y]): + metadata_columns = list(set(metadata_columns + x + y)) + elif x is not None: + metadata_columns = list(set(metadata_columns + x)) + elif y is not None: + metadata_columns = list(set(metadata_columns + y)) + combinations = functions.create_combinations_from_array(sorted(metadata_columns)) + + for var in [x, y]: + if var is not None: + if weight in var: + var.remove(weight) + if all([x, y]): + combinations = [(x_item, y_item) for x_item, y_item in combinations + if x_item in x and y_item in y] + elif x is not None: + combinations = [(x_item, y_item) for x_item, y_item in combinations + if x_item in x] + elif y is not None: + combinations = [(x_item, y_item) for x_item, y_item in combinations + if y_item in y] + + return combinations + + def __create_combinations_no_meta(self, data, data_key, x=None, y=None, weight=None): + if x is None: + x = data.columns.tolist() + if y is None: + y = data.columns.tolist() + for var in [x, y]: + if weight in var: + var.remove(weight) + combinations = [(x_item, y_item) for x_item in x for y_item + in y if x_item != y_item] + self._set_x_and_y_keys(data_key, x, y) + + return combinations + + def __create_links(self, data, data_key, views, variables=None, x=None, y=None, + the_filter=None, store_view_in_link=False, weights=None): + if views is not None: + has_links = True if self[data_key][the_filter].keys() else False + if has_links: + xs = self[data_key][the_filter].keys() + if x is not None: + valid_x = [xk for xk in xs if xk in x] + valid_x.extend(x) + x = set(valid_x) + else: + x = xs + ys = list(set(itertools.chain.from_iterable( + [self[data_key][the_filter][xk].keys() + for xk in xs]))) + if y is not None: + valid_y = [yk for yk in ys if yk in y] + valid_y.extend(y) + y = set(valid_y) + else: + y = ys + if self._x_and_y_keys_in_file(data_key, data, x, y): + for x_key, y_key in itertools.product(x, y): + if x_key==y_key and x_key=='@': + continue + if y_key == '@': + if not isinstance(self[data_key][the_filter][x_key][y_key], Link): + link = Link( + the_filter=the_filter, + x=x_key, + y='@', + data_key=data_key, + stack=self, + store_view=store_view_in_link, + create_views=False + ) + self[data_key][the_filter][x_key]['@'] = link + else: + link = self[data_key][the_filter][x_key]['@'] + elif x_key == '@': + if not isinstance(self[data_key][the_filter][x_key][y_key], Link): + link = Link( + the_filter=the_filter, + x='@', + y=y_key, + data_key=data_key, + stack=self, + store_view=store_view_in_link, + create_views=False + ) + self[data_key][the_filter]['@'][y_key] = link + else: + link = self[data_key][the_filter]['@'][y_key] + else: + if not isinstance(self[data_key][the_filter][x_key][y_key], Link): + link = Link( + the_filter=the_filter, + x=x_key, + y=y_key, + data_key=data_key, + stack=self, + store_view=store_view_in_link, + create_views=False + ) + self[data_key][the_filter][x_key][y_key] = link + else: + link = self[data_key][the_filter][x_key][y_key] + if views is not None: + views._apply_to(link, weights) + + def _x_and_y_keys_in_file(self, data_key, data, x, y): + data_columns = data.columns.tolist() + if '>' in ','.join(y): y = self._clean_from_nests(y) + if '>' in ','.join(x): + raise NotImplementedError('x-axis Nesting not supported.') + x_not_found = [var for var in x if not var in data_columns + and not var == '@'] + y_not_found = [var for var in y if not var in data_columns + and not var == '@'] + if x_not_found is not None: + masks_meta_lookup_x = [var for var in x_not_found + if var in self[data_key].meta['masks'].keys()] + for found_in_meta in masks_meta_lookup_x: + x_not_found.remove(found_in_meta) + if y_not_found is not None: + masks_meta_lookup_y = [var for var in y_not_found + if var in self[data_key].meta['masks'].keys()] + for found_in_meta in masks_meta_lookup_y: + y_not_found.remove(found_in_meta) + if not x_not_found and not y_not_found: + return True + elif x_not_found and y_not_found: + raise ValueError( + 'data key {}: x: {} and y: {} not found.'.format( + data_key, x_not_found, y_not_found)) + elif x_not_found: + raise ValueError( + 'data key {}: x: {} not found.'.format( + data_key, x_not_found)) + elif y_not_found: + raise ValueError( + 'data key {}: y: {} not found.'.format( + data_key, y_not_found)) + + def _clean_from_nests(self, variables): + cleaned = [] + nests = [var for var in variables if '>' in var] + non_nests = [var for var in variables if not '>' in var] + for nest in nests: + cleaned.extend([var.strip() for var in nest.split('>')]) + non_nests += cleaned + non_nests = list(set(non_nests)) + return non_nests + + def __clean_column_names(self, columns): + """ + Remove extra doublequotes if there are any + """ + cols = [] + for column in columns: + cols.append(column.replace('"', '')) + return cols + + def __generate_key_from_list_of(self, list_of_keys): + """ + Generate keys from a list (or tuple). + """ + list_of_keys = list(list_of_keys) + list_of_keys.sort() + return ",".join(list_of_keys) + + def __has_list(self, small): + """ + Check if object contains a list of strings. + """ + keys = self.keys() + for i in xrange(len(keys)-len(small)+1): + for j in xrange(len(small)): + if keys[i+j] != small[j]: + break + else: + return i, i+len(small) + return False + + def __get_all_combinations(self, list_of_items): + """Generates all combinations of items from a list """ + return [itertools.combinations(list_of_items, index+1) + for index in range(len(list_of_items))] + + def __get_stack_pointer(self, stack_pos): + """Takes a stack_pos and returns the stack with that location + raises an exception IF the stack pointer is not found + """ + if self.parent.stack_pos == stack_pos: + return self.parent + else: + return self.parent.__get_stack_pointer(stack_pos) + + def __get_chains(self, name, data_keys, filters, x, y, views, + orientation, select, rules, + rules_weight): + """ + List comprehension wrapper around .get_chain(). + """ + if orientation == 'y': + return [ + self.get_chain( + name=name, + data_keys=data_keys, + filters=filters, + x=x, + y=y_var, + views=views, + select=select, + rules=rules, + rules_weight=rules_weight + ) + for y_var in y + ] + elif orientation == 'x': + return [ + self.get_chain( + name=name, + data_keys=data_keys, + filters=filters, + x=x_var, + y=y, + views=views, + select=select, + rules=rules, + rules_weight=rules_weight + ) + for x_var in x + ] + else: + raise ValueError( + "Unknown orientation type. Please use 'x' or 'y'." + ) + + def _verify_multiple_key_types(self, data_keys=None, filters=None, x=None, + y=None, variables=None, views=None): + """ + Verify that the given keys str or unicode or a list or tuple of those. + """ + if data_keys is not None: + self._verify_key_types(name='data', keys=data_keys) + + if filters is not None: + self._verify_key_types(name='filter', keys=filters) + + if x is not None: + self._verify_key_types(name='x', keys=x) + + if y is not None: + self._verify_key_types(name='y', keys=y) + + if variables is not None: + self._verify_key_types(name='variables', keys=variables) + + if views is not None: + self._verify_key_types(name='view', keys=views) + + def _verify_key_exists(self, key, stack_path=[]): + """ + Verify that the given key exists in the stack at the path targeted. + """ + error_msg = ( + "Could not find the {key_type} key '{key}' in: {stack_path}. " + "Found {keys_found} instead." + ) + try: + dk = stack_path[0] + fk = stack_path[1] + xk = stack_path[2] + yk = stack_path[3] + vk = stack_path[4] + except: + pass + try: + if len(stack_path) == 0: + if key not in self: + key_type, keys_found = 'data', self.keys() + stack_path = 'stack' + raise ValueError + elif len(stack_path) == 1: + if key not in self[dk]: + key_type, keys_found = 'filter', self[dk].keys() + stack_path = "stack['{dk}']".format( + dk=dk) + raise ValueError + elif len(stack_path) == 2: + if key not in self[dk][fk]: + key_type, keys_found = 'x', self[dk][fk].keys() + stack_path = "stack['{dk}']['{fk}']".format( + dk=dk, fk=fk) + raise ValueError + elif len(stack_path) == 3: + meta = self[dk].meta + if self._is_array_summary(meta, xk, None) and not key == '@': + pass + elif key not in self[dk][fk][xk]: + key_type, keys_found = 'y', self[dk][fk][xk].keys() + stack_path = "stack['{dk}']['{fk}']['{xk}']".format( + dk=dk, fk=fk, xk=xk) + raise ValueError + elif len(stack_path) == 4: + if key not in self[dk][fk][xk][yk]: + key_type, keys_found = 'view', self[dk][fk][xk][yk].keys() + stack_path = "stack['{dk}']['{fk}']['{xk}']['{yk}']".format( + dk=dk, fk=fk, xk=xk, yk=yk) + raise ValueError + except ValueError: + print error_msg.format( + key_type=key_type, + key=key, + stack_path=stack_path, + keys_found=keys_found + ) + + def _force_key_as_list(self, key): + """Returns key as [key] if it is str or unicode""" + return [key] if isinstance(key, (str, unicode)) else key + + def _verify_key_types(self, name, keys): + """ + Verify that the given keys str or unicode or a list or tuple of those. + """ + if isinstance(keys, (list, tuple)): + for key in keys: + self._verify_key_types(name, key) + elif isinstance(keys, (str, unicode)): + pass + else: + raise TypeError( + "All %s keys must be one of the following types: " + " or , " + " of or , " + " of or . " + "Given: %s" % (name, keys) + ) + + def _find_groups(self, view): + groups = OrderedDict() + logic = view._kwargs.get('logic') + description = view.describe_block() + groups['codes'] = [c for c, d in description.items() if d == 'normal'] + net_names = [v for v, d in description.items() if d == 'net'] + for l in logic: + new_l = copy.deepcopy(l) + for k in l: + if k not in net_names: + del new_l[k] + groups[new_l.keys()[0]] = new_l.values()[0] + groups['codes'] = [c for c, d in description.items() if d == 'normal'] + return groups + + def sort_expanded_nets(self, view, within=True, between=True, ascending=False, + fix=None): + if not within and not between: + return view.dataframe + df = view.dataframe + name = df.index.levels[0][0] + if not fix: + fix_codes = [] + else: + if not isinstance(fix, list): + fix_codes = [fix] + else: + fix_codes = fix + fix_codes = [c for c in fix_codes if c in + df.index.get_level_values(1).tolist()] + net_groups = self._find_groups(view) + sort_col = (df.columns.levels[0][0], '@') + sort = [(name, v) for v in df.index.get_level_values(1) + if (v in net_groups['codes'] or + v in net_groups.keys()) and not v in fix_codes] + if between: + if pd.__version__ == '0.19.2': + temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) + else: + temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) + else: + temp_df = df.loc[sort] + between_order = temp_df.index.get_level_values(1).tolist() + code_group_list = [] + for g in between_order: + if g in net_groups: + code_group_list.append([g] + net_groups[g]) + elif g in net_groups['codes']: + code_group_list.append([g]) + final_index = [] + for g in code_group_list: + is_code = len(g) == 1 + if not is_code: + fixed_net_name = g[0] + sort = [(name, v) for v in g[1:]] + if within: + if pd.__version__ == '0.19.2': + temp_df = df.loc[sort].sort_values(sort_col, 0, ascending=ascending) + else: + temp_df = df.loc[sort].sort_index(0, sort_col, ascending=ascending) + else: + temp_df = df.loc[sort] + new_idx = [fixed_net_name] + temp_df.index.get_level_values(1).tolist() + final_index.extend(new_idx) + else: + final_index.extend(g) + final_index = [(name, i) for i in final_index] + if fix_codes: + fix_codes = [(name, f) for f in fix_codes] + final_index.extend(fix_codes) + df = df.reindex(final_index) + return df + + def get_frequency_via_stack(self, data_key, the_filter, col, weight=None): + weight_notation = '' if weight is None else weight + vk = 'x|f|:||{}|counts'.format(weight_notation) + try: + f = self[data_key][the_filter][col]['@'][vk].dataframe + except (KeyError, AttributeError) as e: + try: + f = self[data_key][the_filter]['@'][col][vk].dataframe.T + except (KeyError, AttributeError) as e: + f = frequency(self[data_key].meta, self[data_key].data, x=col, weight=weight) + return f + + def get_descriptive_via_stack(self, data_key, the_filter, col, weight=None): + l = self[data_key][the_filter][col]['@'] + w = '' if weight is None else weight + mean_key = [k for k in l.keys() if 'd.mean' in k.split('|')[1] and + k.split('|')[-2] == w] + if not mean_key: + msg = "No mean view to sort '{}' on found!" + raise RuntimeError(msg.format(col)) + elif len(mean_key) > 1: + msg = "Multiple mean views found for '{}'. Unable to sort!" + raise RuntimeError(msg.format(col)) + else: + mean_key = mean_key[0] + vk = mean_key + d = l[mean_key].dataframe + return d + + def _is_array_summary(self, meta, x, y): + return x in meta['masks'] + + def _is_transposed_summary(self, meta, x, y): + return x == '@' and y in meta['masks'] + + def axis_slicer_from_vartype(self, all_rules_axes, rules_axis, dk, the_filter, x, y, rules_weight): + if rules_axis == 'x' and 'x' not in all_rules_axes: + return None + elif rules_axis == 'y' and 'y' not in all_rules_axes: + return None + meta = self[dk].meta + + array_summary = self._is_array_summary(meta, x, y) + transposed_summary = self._is_transposed_summary(meta, x, y) + + axis_slicer = None + + if rules_axis == 'x': + if not array_summary and not transposed_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, x=x, weight=rules_weight) + elif array_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, x=x, y='@', weight=rules_weight, + slice_array_items=True) + elif transposed_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, x='@', y=y, weight=rules_weight) + elif rules_axis == 'y': + if not array_summary and not transposed_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, y=y, weight=rules_weight) + elif array_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, x=x, y='@', weight=rules_weight, + slice_array_items=False) + elif transposed_summary: + axis_slicer = self.get_rules_slicer_via_stack( + dk, the_filter, x='@', y=y, weight=rules_weight) + + return axis_slicer + + def get_rules_slicer_via_stack(self, data_key, the_filter, + x=None, y=None, weight=None, + slice_array_items=False): + m = self[data_key].meta + array_summary = self._is_array_summary(m, x, y) + transposed_summary = self._is_transposed_summary(m, x, y) + + rules = None + + if not array_summary and not transposed_summary: + if not x is None: + try: + rules = self[data_key].meta['columns'][x]['rules']['x'] + col = x + except: + pass + elif not y is None: + try: + rules = self[data_key].meta['columns'][y]['rules']['y'] + col = y + except: + pass + + elif array_summary: + if slice_array_items: + try: + rules = self[data_key].meta['masks'][x]['rules']['x'] + col = x + except: + pass + else: + try: + rules = self[data_key].meta['masks'][x]['rules']['y'] + col = x + except: + pass + + elif transposed_summary: + try: + rules = self[data_key].meta['masks'][y]['rules']['x'] + col = y + except: + pass + + if not rules: return None + views = self[data_key][the_filter][col]['@'].keys() + w = '' if weight is None else weight + expanded_net = [v for v in views if '}+]' in v + and v.split('|')[-2] == w + and v.split('|')[1] == 'f' and + not v.split('|')[3] == 'x'] + if expanded_net: + if len(expanded_net) > 1: + if len(expanded_net) == 2: + if expanded_net[0].split('|')[2] == expanded_net[1].split('|')[2]: + expanded_net = expanded_net[0] + else: + msg = "Multiple 'expand' using views found for '{}'. Unable to sort!" + raise RuntimeError(msg.format(col)) + else: + expanded_net = expanded_net[0] + if 'sortx' in rules: + on_mean = rules['sortx'].get('sort_on', '@') == 'mean' + else: + on_mean = False + if 'sortx' in rules and on_mean: + f = self.get_descriptive_via_stack( + data_key, the_filter, col, weight=weight) + elif 'sortx' in rules and expanded_net: + within = rules['sortx'].get('within', False) + between = rules['sortx'].get('between', False) + fix = rules['sortx'].get('fixed', False) + ascending = rules['sortx'].get('ascending', False) + view = self[data_key][the_filter][col]['@'][expanded_net] + f = self.sort_expanded_nets(view, between=between, within=within, + ascending=ascending, fix=fix) + else: + f = self.get_frequency_via_stack( + data_key, the_filter, col, weight=weight) + + if transposed_summary or (not slice_array_items and array_summary): + rules_slicer = functions.get_rules_slicer(f.T, rules) + else: + if not expanded_net or ('sortx' in rules and on_mean): + rules_slicer = functions.get_rules_slicer(f, rules) + else: + rules_slicer = f.index.values.tolist() + try: + rules_slicer.remove((col, 'All')) + except: + pass + return rules_slicer + + @modify(to_list='batches') + def _check_batches(self, dk, batches='all'): + """ + Returns a list of valid ``qp.Batch`` names. + + Parameters + ---------- + batches: str/ list of str, default 'all' + Included names are checked against valid ``qp.Batch`` names. If + batches='all', all valid ``Batch`` names are returned. + + Returns + ------- + list of str + """ + if not batches: + return [] + elif batches[0] == 'all': + return self[dk].meta['sets']['batches'].keys() + else: + valid = self[dk].meta['sets']['batches'].keys() + not_valid = [b for b in batches if not b in valid] + if not_valid: + msg = '``Batch`` name not found in ``Stack``: {}' + raise KeyError(msg.format(not_valid)) + return batches + + def _x_y_f_w_map(self, dk, batches='all'): + """ + """ + def _append_loop(mapping, x, fi, w, ys): + if fi: fi = fi.encode('utf8') + fn = 'no_filter' if fi is None else fi + f = 'no_filter' if fi is None else {fi: {fi: 0}} + if not x in mapping: + mapping[x] = {fn: {'f': f, tuple(w): ys}} + elif not fn in mapping[x]: + mapping[x][fn] = {'f': f, tuple(w): ys} + elif not tuple(w) in mapping[x][fn]: + mapping[x][fn][tuple(w)] = ys + elif not all(y in mapping[x][fn][tuple(w)] for y in ys): + yks = set(mapping[x][fn][tuple(w)]).union(set(ys)) + mapping[x][fn][tuple(w)] = list(yks) + return None + + arrays = self.variable_types(dk, verbose=False)['array'] + mapping = {} + y_on_y = {} + batches = self._check_batches(dk, batches) + for batch in batches: + b = self[dk].meta['sets']['batches'][batch] + xy = b['x_y_map'] + f = b['x_filter_map'] + fy = b['y_filter_map'] + w = b['weights'] + for x, y in xy: + if x == '@': + y = y[0] + _append_loop(mapping, x, f[y], w, [y]) + else: + _append_loop(mapping, x, f[x], w, y) + for yy in b['y_on_y']: + for x in b['yks'][1:]: + _append_loop(mapping, x, fy[yy], w, b['yks']) + _append_loop(y_on_y, x, fy[yy], w, b['yks']) + return mapping, y_on_y + + @modify(to_list=['views', 'categorize', 'xs', 'batches']) + def aggregate(self, views, unweighted_base=True, categorize=[], + batches='all', xs=None, bases={}, verbose=True): + + """ + Add views to all defined ``qp.Link`` in ``qp.Stack``. + + Parameters + ---------- + views: str or list of str or qp.ViewMapper + ``views`` that are added. + unweighted_base: bool, default True + If True, unweighted 'cbase' is added to all non-arrays. + This parameter will be deprecated in future, please use bases + instead. + categorize: str or list of str + Determines how numerical data is handled: If provided, the + variables will get counts and percentage aggregations + (``'counts'``, ``'c%'``) alongside the ``'cbase'`` view. If False, + only ``'cbase'`` views are generated for non-categorical types. + batches: str/ list of str, default 'all' + Name(s) of ``qp.Batch`` instance(s) that are used to aggregate the + ``qp.Stack``. + xs: list of str + Names of variable, for which views are added. + bases: dict + Defines which bases should be aggregated, weighted or unweighted. + + Returns + ------- + None, modify ``qp.Stack`` inplace + """ + # Preparing bases if older version with unweighed_base is used + valid_bases = ['cbase', 'cbase_gross', 'ebase'] + if not bases and any(v in valid_bases for v in views): + new_bases = {} + for ba in valid_bases: + if ba in views: + new_bases[ba] = {'unwgt': False if ba=='ebase' else unweighted_base, + 'wgt': True} + views = [v for v in views if not v in valid_bases] + else: + new_bases = bases + + # Check if views are complete + if views and isinstance(views[0], ViewMapper): + views = views[0] + complete = views[views.keys()[0]]['kwargs'].get('complete', False) + elif any('cumsum' in v for v in views): + complete = True + else: + complete = False + + # get counts + net views + count_net_views = ['counts', 'counts_sum', 'counts_cumsum'] + if isinstance(views, ViewMapper) and views.keys() == ['net']: + counts_nets = qp.ViewMapper() + counts_nets.make_template('frequency', {'rel_to': [None, 'y']}) + options = {'logic': views['net']['kwargs']['logic'], + 'axis': 'x', + 'expand': views['net']['kwargs']['expand'], + 'complete': views['net']['kwargs']['complete'], + 'calc': views['net']['kwargs']['calc']} + counts_nets.add_method('net', kwargs=options) + else: + counts_nets = [v for v in views if v in count_net_views] + + x_in_stack = self.describe('x').index.tolist() + for dk in self.keys(): + batches = self._check_batches(dk, batches) + if not batches: return None + # check for unweighted_counts + batch = self[dk].meta['sets']['batches'] + unwgt_c = any(batch[b].get('unwgt_counts') for b in batches) + # get map and conditions for aggregation + x_y_f_w_map, y_on_y = self._x_y_f_w_map(dk, batches) + if not xs: + xs = [x for x in x_y_f_w_map.keys() if x in x_in_stack] + else: + xs = [x for x in xs if x in x_in_stack or isinstance(x, tuple)] + v_typ = self.variable_types(dk, verbose=False) + numerics = v_typ['int'] + v_typ['float'] + masks = self[dk].meta['masks'] + num_arrays = [m for m in masks if masks[m]['subtype'] in ['int', 'float']] + if num_arrays: numerics = numerics + num_arrays + skipped = [x for x in xs if (x in numerics and not x in categorize) + and not isinstance(x, tuple)] + total_len = len(xs) + # loop over map and aggregate views + if total_len == 0: + msg = "Cannot aggregate, 'xs' contains no valid variables." + raise ValueError(msg) + for idx, x in enumerate(xs, start=1): + y_trans = None + if isinstance(x, tuple): + y_trans = x[1] + x = x[0] + if not x in x_y_f_w_map.keys(): + msg = "Cannot find {} in qp.Stack for ``qp.Batch`` '{}'" + raise KeyError(msg.format(x, batches)) + v = [] if x in skipped else views + for f_dict in x_y_f_w_map[x].values(): + f = f_dict['f'] + f_key = f.keys()[0] if isinstance(f, dict) else f + for weight, y in f_dict.items(): + if weight == 'f': continue + if y_trans: y = y_trans + w = list(weight) if weight else None + # add bases + for ba, weights in new_bases.items(): + ba_w = [b_w for b_w in w if not b_w is None] + if weights.get('wgt') and ba_w: + self.add_link(dk, f, x=x, y=y, views=[ba], weights=ba_w) + if (weights.get('wgt') and not ba_w) or weights.get('unwgt'): + self.add_link(dk, f, x=x, y=y, views=[ba], weights=None) + # remove existing nets for link if new view is a net + if isinstance(v, ViewMapper) and v.get('net') and not y_trans: + for ys in y: + link = self[dk][f_key][x][ys] + for view in link.keys(): + is_net = view.split('|')[-1] == 'net' + has_w = view.split('|')[-2] + if not has_w: has_w = None + if is_net and has_w in f_dict.keys(): + del link[view] + # add unweighted views for counts/ nets + if unwgt_c and counts_nets and not None in w: + self.add_link(dk, f, x=x, y=y, views=counts_nets, weights=None) + # add common views + self.add_link(dk, f, x=x, y=y, views=v, weights=w) + # remove views if complete (cumsum/ nets) + if complete: + for ys in y: + y_on_ys = y_on_y.get(x, {}).get(f_key, {}).get(tuple(w), []) + if ys in y_on_ys: continue + link = self[dk][f_key][x][ys] + for ws in w: + pct = 'x|f|:|y|{}|c%'.format('' if not ws else ws) + counts = 'x|f|:||{}|counts'.format('' if not ws else ws) + for view in [pct, counts]: + if view in link: + del link[view] + if verbose: + done = float(idx) / float(total_len) *100 + print '\r', + time.sleep(0.01) + print 'Stack [{}]: {} %'.format(dk, round(done, 1)), + sys.stdout.flush() + print '\n' + + if skipped and verbose: + msg = ("\n\nWarning: Found {} non-categorized numeric variable(s): {}.\n" + "Descriptive statistics must be added!") + print msg.format(len(skipped), skipped) + return None + + @modify(to_list=['on_vars', '_batches']) + def cumulative_sum(self, on_vars, _batches='all', verbose=True): + """ + Add cumulative sum view to a specified collection of xks of the stack. + + Parameters + ---------- + on_vars : list + The list of x variables to add the view to. + _batches: str or list of str + Only for ``qp.Links`` that are defined in this ``qp.Batch`` + instances views are added. + + Returns + ------- + None + The stack instance is modified inplace. + """ + for dk in self.keys(): + _batches = self._check_batches(dk, _batches) + if not _batches or not on_vars: return None + meta = self[dk].meta + data = self[dk].data + for v in on_vars: + if v in meta['sets']: + items = [i.split('@')[-1] for i in meta['sets'][v]['items']] + on_vars = list(set(on_vars + items)) + + self.aggregate(['counts_cumsum', 'c%_cumsum'], False, [], _batches, on_vars, verbose=verbose) + return None + + def _add_checking_chain(self, dk, cluster, name, x, y, views): + key, view, c_view = views + if isinstance(cluster, ChainManager): + c_stack = cluster.stack + else: + c_stack = qp.Stack('checks') + c_stack.add_data('checks', data=self[dk].data, meta=self[dk].meta) + c_stack.add_link(x=x, y=y, views=view, weights=None) + c_stack.add_link(x=x, y=y, views=c_view, weights=None) + c_views = c_stack.describe('view').index.tolist() + len_v_keys = len(view) + view_keys = ['x|f|x:|||cbase', 'x|f|:|||counts'][0:len_v_keys] + c_views = view_keys + [v for v in c_views + if v.endswith('{}_check'.format(key))] + if isinstance(cluster, ChainManager): + cluster.get('checks', 'no_filter', x, y, c_views, folder=name) + else: + if name == 'stat_check': + chain = c_stack.get_chain(x=x, y=y, views=c_views, orient_on='x') + name = [v for v in c_views if v.endswith('{}_check'.format(key))][0] + cluster[name] = chain + else: + chain = c_stack.get_chain(name=name, x=x, y=y, views=c_views) + cluster.add_chain(chain) + return cluster + + @staticmethod + def recode_from_net_def(dataset, on_vars, net_map, expand, recode='auto', + text_prefix='Net:', verbose=True): + """ + Create variables from net definitions. + """ + def _is_simple_net(net_map): + return all(isinstance(net.values()[0], list) for net in net_map) + + def _dissect_defs(ds, var, net_map, recode, text_prefix): + mapper = [] + if recode == 'extend_codes': + mapper += [(x, y, {var: x}) for (x,y) in ds.values(var)] + max_code = max(ds.codes(var)) + elif recode == 'drop_codes': + max_code = 0 + elif 'collect_codes' in recode: + max_code = 0 + + appends = [] + labels = {} + s_net = True + simple_nets = [] + for x, net in enumerate(net_map, 1): + n = copy.deepcopy(net) + if net.get('text'): + labs = n.pop('text') + else: + labs = {ds.text_key: n.keys()[0]} + code = max_code + x + for tk, lab in labs.items(): + if not tk in labels: labels[tk] = {} + labels[tk].update({code: '{} {}'.format(text_prefix, lab)}) + appends.append((code, str(code), {var: n.values()[0]})) + if not isinstance(n.values()[0], list): + s_net = False + simple_nets = [] + if s_net: + simple_nets.append( + ('{} {}'.format(text_prefix, labs[ds.text_key]), n.values()[0])) + mapper += appends + q_type = 'delimited set' if ds._is_delimited_set_mapper(mapper) else 'single' + return mapper, q_type, labels, simple_nets + + forced_recode = False + valid = ['extend_codes', 'drop_codes', 'collect_codes'] + if recode == 'auto': + recode = 'collect_codes' + forced_recode = True + if not any(rec in recode for rec in valid): + raise ValueError("'recode' must be one of {}".format(valid)) + + dataset._meta['sets']['to_array'] = {} + for var in on_vars[:]: + if dataset.is_array(var): continue + # get name for new variable + suffix = '_rc' + for s in [str(x) if not x == 1 else '' for x in frange('1-5')]: + suf = suffix + s + name = '{}{}'.format(dataset._dims_free_arr_item_name(var), suf) + if dataset.var_exists(name): + if dataset._meta['columns'][name]['properties'].get('recoded_net'): + break + else: + break + + # collect array items + if dataset._is_array_item(var): + to_array_set = dataset._meta['sets']['to_array'] + parent = dataset._maskname_from_item(var) + arr_name = dataset._dims_free_arr_name(parent) + suf + if arr_name in dataset: + msg = "Cannot create array {}. Variable already exists!" + if not dataset.get_property(arr_name, 'recoded_net'): + raise ValueError(msg.format(arr_name)) + no = dataset.item_no(var) + if not arr_name in to_array_set: + to_array_set[arr_name] = [parent, [name], [no]] + else: + to_array_set[arr_name][1].append(name) + to_array_set[arr_name][2].append(no) + + # create mapper to derive new variable + mapper, q_type, labels, simple_nets = _dissect_defs( + dataset, var, net_map, recode, text_prefix) + dataset.derive(name, q_type, dataset.text(var), mapper) + + # meta edits for new variable + for tk, labs in labels.items(): + dataset.set_value_texts(name, labs, tk) + text = dataset.text(var, tk) or dataset.text(var, None) + dataset.set_variable_text(name, text, tk) + + # properties + props = dataset._meta['columns'][name]['properties'] + props.update({'recoded_net': var}) + if 'properties' in dataset._meta['columns'][var]: + for pname, prop in dataset._meta['columns'][var]['properties'].items(): + if pname == 'survey': continue + props[pname] = prop + if simple_nets: + props['simple_org_expr'] = simple_nets + + if verbose: + print 'Created: {}'. format(name) + if forced_recode: + warnings.warn("'{}' was a forced recode.".format(name)) + + # order, remove codes + if 'collect_codes' in recode: + other_logic = intersection( + [{var: not_count(0)}, {name: has_count(0)}]) + if dataset._is_array_item(var) or dataset.take(other_logic).tolist(): + cat_name = recode.split('@')[-1] if '@' in recode else 'Other' + code = len(mapper)+1 + dataset.extend_values(name, [(code, str(code))]) + for tk in labels.keys(): + dataset.set_value_texts(name, {code: cat_name}, tk) + dataset.recode(name, {code: other_logic}) + if recode == 'extend_codes' and expand: + codes = dataset.codes(var) + new = [c for c in dataset.codes(name) if not c in codes] + order = [] + remove = [] + for x, y, z in mapper[:]: + if not x in new: + order.append(x) + else: + vals = z.values()[0] + if not isinstance(vals, list): + remove.append(vals) + vals = [vals] + if expand == 'after': + idx = order.index(codes[min([codes.index(v) for v in vals])]) + elif expand == 'before': + idx = order.index(codes[max([codes.index(v) for v in vals])]) + 1 + order.insert(idx, x) + + dataset.reorder_values(name, order) + dataset.remove_values(name, remove) + + for arr_name, arr_items in dataset._meta['sets']['to_array'].items(): + org_mask = arr_items[0] + m_items = arr_items[1] + m_order = arr_items[2] + m_items = [item[1] for item in sorted(zip(m_order, m_items))] + dataset.to_array(arr_name, m_items, '', False) + dims_name = dataset._dims_compat_arr_name(arr_name) + prop = dataset._meta['masks'][dims_name]['properties'] + prop['recoded_net'] = org_mask + if 'properties' in dataset._meta['masks'][org_mask]: + for p, v in dataset._meta['masks'][org_mask]['properties'].items(): + if p == 'survey': continue + prop[p] = v + n_i0 = dataset.sources(dims_name)[0] + simple_net = dataset._meta['columns'][n_i0]['properties'].get('simple_org_expr') + if simple_net: + dataset._meta['masks'][dims_name]['properties'].update( + {'simple_org_expr': simple_net}) + if verbose: + msg = "Array {} built from recoded view variables!" + print msg.format(dims_name) + del dataset._meta['sets']['to_array'] + + return None + + + @modify(to_list=['on_vars', '_batches']) + def add_nets(self, on_vars, net_map, expand=None, calc=None, text_prefix='Net:', + checking_cluster=None, _batches='all', recode='auto', verbose=True): + """ + Add a net-like view to a specified collection of x keys of the stack. + + Parameters + ---------- + on_vars : list + The list of x variables to add the view to. + net_map : list of dicts + The listed dicts must map the net/band text label to lists of + categorical answer codes to group together, e.g.: + + >>> [{'Top3': [1, 2, 3]}, + ... {'Bottom3': [4, 5, 6]}] + It is also possible to provide enumerated net definition dictionaries + that are explicitly setting ``text`` metadata per ``text_key`` entries: + + >>> [{1: [1, 2], 'text': {'en-GB': 'UK NET TEXT', + ... 'da-DK': 'DK NET TEXT', + ... 'de-DE': 'DE NET TEXT'}}] + expand : {'before', 'after'}, default None + If provided, the view will list the net-defining codes after or before + the computed net groups (i.e. "overcode" nets). + calc : dict, default None + A dictionary that is attaching a text label to a calculation expression + using the the net definitions. The nets are referenced as per + 'net_1', 'net_2', 'net_3', ... . + Supported calculation expressions are add, sub, div, mul. Example: + + >>> {'calc': ('net_1', add, 'net_2'), 'text': {'en-GB': 'UK CALC LAB', + ... 'da-DK': 'DA CALC LAB', + ... 'de-DE': 'DE CALC LAB'}} + text_prefix : str, default 'Net:' + By default each code grouping/net will have its ``text`` label prefixed + with 'Net: '. Toggle by passing None (or an empty str, ''). + checking_cluster : quantipy.Cluster, default None + When provided, an automated checking aggregation will be added to the + ``Cluster`` instance. + _batches: str or list of str + Only for ``qp.Links`` that are defined in this ``qp.Batch`` + instances views are added. + recode: {'extend_codes', 'drop_codes', 'collect_codes', 'collect_codes@cat_name'}, + default 'auto' + Adds variable with nets as codes to DataSet/Stack. If 'extend_codes', + codes are extended with nets. If 'drop_codes', new variable only + contains nets as codes. If 'collect_codes' or 'collect_codes@cat_name' + the variable contains nets and another category that summarises all + codes which are not included in any net. If no cat_name is provided, + 'Other' is taken as default + Returns + ------- + None + The stack instance is modified inplace. + """ + + def _netdef_from_map(net_map, expand, prefix, text_key): + netdef = [] + for no, net in enumerate(net_map, start=1): + if 'text' in net: + logic = net[no] + text = net['text'] + else: + logic = net.values()[0] + text = {t: net.keys()[0] for t in text_key} + if not isinstance(logic, list) and isinstance(logic, int): + logic = [logic] + if prefix and not expand: + text = {k: '{} {}'.format(prefix, v) for k, v in text.items()} + if expand: + text = {k: '{} (NET)'.format(v) for k, v in text.items()} + netdef.append({'net_{}'.format(no): logic, 'text': text}) + return netdef + + def _check_and_update_calc(calc_expression, text_key): + if not isinstance(calc_expression, dict): + err_msg = ("'calc' must be a dict in form of\n" + "{'calculation label': (net # 1, operator, net # 2)}") + raise TypeError(err_msg) + for k, v in calc_expression.items(): + if not k in ['text', 'calc_only']: exp = v + if not k == 'calc_only': text = v + if not 'text' in calc_expression: + text = {tk: text for tk in text_key} + calc_expression['text'] = text + if not isinstance(exp, (tuple, list)) or len(exp) != 3: + err_msg = ("Not properly formed expression found in 'calc':\n" + "{}\nMust be provided as (net # 1, operator, net # 2)") + raise TypeError(err_msg.format(exp)) + return calc_expression + + for dk in self.keys(): + _batches = self._check_batches(dk, _batches) + only_recode = not _batches and recode + if not _batches and not recode: return None + meta = self[dk].meta + data = self[dk].data + check_on = [] + for v in on_vars[:]: + if v in meta['sets']: + items = [i.split('@')[-1] for i in meta['sets'][v]['items']] + on_vars = list(set(on_vars)) + items + check_on.append(items[0]) + elif meta['columns'][v].get('parent'): + msg = 'Nets can not be added to a single array item: {}' + raise ValueError(msg.format(v)) + else: + check_on.append(v) + if any(v in meta['sets']['batches'][b]['transposed_arrays'] + for b in _batches): + on_vars += [('@', v)] + + if not only_recode: + all_batches = copy.deepcopy(meta['sets']['batches']) + for n, b in all_batches.items(): + if not n in _batches: all_batches.pop(n) + languages = list(set(b['language'] for n, b in all_batches.items())) + netdef = _netdef_from_map(net_map, expand, text_prefix, languages) + if calc: + calc = _check_and_update_calc(calc, languages) + calc_only = calc.get('calc_only', False) + else: + calc_only = False + view = qp.ViewMapper() + view.make_template('frequency', {'rel_to': [None, 'y']}) + + options = {'logic': netdef, + 'axis': 'x', + 'expand': expand if expand in ['after', 'before'] else None, + 'complete': True if expand else False, + 'calc': calc, + 'calc_only': calc_only} + view.add_method('net', kwargs=options) + self.aggregate(view, False, [], _batches, on_vars, verbose=verbose) + + if recode: + ds = ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) + ds.from_stack(self, dk) + on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] + self.recode_from_net_def(ds, on_vars, net_map, expand, recode, + text_prefix, verbose) + + if checking_cluster in [None, False] or only_recode: continue + if isinstance(checking_cluster, ChainManager): + cc_keys = checking_cluster.folder_names + else: + cc_keys = checking_cluster.keys() + view['net_check'] = view.pop('net') + view['net_check']['kwargs']['iterators'].pop('rel_to') + for v in check_on: + v_net = '{}_net'.format(v) + v_net = v_net.split('.')[-1] + if not v_net in cc_keys: + checking_cluster = self._add_checking_chain(dk, checking_cluster, + v_net, v, ['@', v], ('net', ['cbase'], view)) + + return None + + @staticmethod + def _factor_labs(values, axis, rescale, drop, exclude, factor_labels, + has_factors): + if not rescale: rescale = {} + ignore = [v['value'] for v in values if v['value'] in exclude or + (not v['value'] in rescale.keys() and drop)] + if factor_labels == '()': + new_lab = '{} ({})' + split = ('(', ')') + else: + new_lab = '{} [{}]' + split = ('[', ']') + factors_mapped = {} + for v in values: + if v['value'] in ignore: continue + has_xedits = v['text'].get('x edits', {}) + has_yedits = v['text'].get('y edits', {}) + if not has_xedits: v['text']['x edits'] = {} + if not has_yedits: v['text']['y edits'] = {} + + factor = rescale[v['value']] if rescale else v['value'] + for tk, text in v['text'].items(): + if tk in ['x edits', 'y edits']: continue + for ax in axis: + try: + t = v['text']['{} edits'.format(ax)][tk] + except: + t = text + if has_factors: + fac = t.split(split[0])[-1].replace(split[1], '') + if fac == str(factor): continue + v['text']['{} edits'.format(ax)][tk] = new_lab.format(t, factor) + return values + + @staticmethod + def _add_factor_meta(dataset, var, options): + if not dataset._has_categorical_data(var): + return None + rescale = options[0] + drop = options[1] + exclude = options[2] + dataset.clear_factors(var) + all_codes = dataset.codes(var) + if rescale: + fm = rescale + else: + fm = {c: c for c in all_codes} + if not drop and rescale: + for c in all_codes: + if not c in fm: + fm[c] = c + if exclude: + for e in exclude: + if e in fm: + del fm[e] + dataset.set_factors(var, fm) + return None + + @modify(to_list=['on_vars', 'stats', 'exclude', '_batches']) + def add_stats(self, on_vars, stats=['mean'], other_source=None, rescale=None, + drop=True, exclude=None, factor_labels=True, custom_text=None, + checking_cluster=None, _batches='all', recode=False, verbose=True): + """ + Add a descriptives view to a specified collection of xks of the stack. + + Valid descriptives views: {'mean', 'stddev', 'min', 'max', 'median', 'sem'} + + Parameters + ---------- + on_vars : list + The list of x variables to add the view to. + stats : list of str, default ``['mean']`` + The metrics to compute and add as a view. + other_source : str + If provided the Link's x-axis variable will be swapped with the + (numerical) variable provided. This can be used to attach statistics + of a different variable to a Link definition. + rescale : dict + A dict that maps old to new codes, e.g. {1: 5, 2: 4, 3: 3, 4: 2, 5: 1} + drop : bool, default True + If ``rescale`` is provided all codes that are not mapped will be + ignored in the computation. + exclude : list + Codes/values to ignore in the computation. + factor_labels : bool / str, default True + Writes the (rescaled) factor values next to the category text label. + If True, square-brackets are used. If '()', normal brackets are used. + custom_text : str, default None + A custom string affix to put at the end of the requested statistics' + names. + checking_cluster : quantipy.Cluster, default None + When provided, an automated checking aggregation will be added to the + ``Cluster`` instance. + _batches: str or list of str + Only for ``qp.Links`` that are defined in this ``qp.Batch`` + instances views are added. + recode: bool, default False + Create a new variable that contains only the values + which are needed for the stat computation. The values and the included + data will be rescaled. + + Returns + ------- + None + The stack instance is modified inplace. + """ + + def _recode_from_stat_def(dataset, on_vars, rescale, drop, exclude, verbose): + for var in on_vars: + if dataset.is_array(var): continue + suffix = '_rc' + for s in [str(x) if not x == 1 else '' for x in frange('1-5')]: + suf = suffix + s + name = '{}{}'.format(var, suf) + if dataset.var_exists(name): + if dataset._meta['columns'][name]['properties'].get('recoded_stat'): + break + else: + break + if not rescale: + rescale = {x: x for x in dataset.codes(var)} + else: + rescale = copy.deepcopy(rescale) + if drop or exclude: + for x in rescale.keys(): + if not x in dataset.codes(var) or x in exclude: + rescale.pop(x) + dataset.add_meta(name, 'float', dataset.text(var)) + for x, y in rescale.items(): + sl = dataset.take({var: x}) + dataset[sl, name] = y + if verbose: + print 'Created: {}'. format(name) + dataset._meta['columns'][name]['properties'].update({'recoded_stat': var}) + return None + + def _add_factors(v, meta, values, args): + if isinstance(values, basestring): + p = values.split('@')[-1] + p_meta = meta.get('masks', meta)[p] + p_lib = meta['lib'].get('values', meta['lib']) + has_factors = p_meta.get('properties', {}).get('factor_labels', False) + v_args = args + [has_factors] + values = p_lib[p] + p_lib[p] = self._factor_labs(values, ['x', 'y'], *v_args) + if not p_meta.get('properties'): p_meta['properties'] = {} + p_meta['properties'].update({'factor_labels': True}) + else: + v_meta = meta.get('columns', meta)[v] + has_factors = v_meta.get('properties', {}).get('factor_labels') + v_args = args + [has_factors] + v_meta['values'] = self._factor_labs(values, ['x'], *v_args) + if not v_meta.get('properties'): v_meta['properties'] = {} + v_meta['properties'].update({'factor_labels': True}) + return None + + if other_source and not isinstance(other_source, str): + raise ValueError("'other_source' must be a str!") + if not rescale: drop = False + + options = {'stats': '', + 'source': other_source, + 'rescale': rescale, + 'drop': drop, 'exclude': exclude, + 'axis': 'x', + 'text': '' if not custom_text else custom_text} + warn = "\nCannot add stats on '{}'.\n" + for dk in self.keys(): + _batches = self._check_batches(dk, _batches) + if not _batches: return None + meta = self[dk].meta + data = self[dk].data + check_on = [] + for v in on_vars: + if v in meta['sets']: + if meta['masks'][v]['subtype'] == 'delimited set': + w = warn + 'Stats are not valid on delimited sets!\n' + print w.format(v) + continue + items = [i.split('@')[-1] for i in meta['sets'][v]['items']] + on_vars = list(set(on_vars + items)) + check_on = list(set(check_on + [items[0]])) + elif not meta['columns'][v].get('values'): + w = warn + 'No values found!\n' + print w.format(v) + continue + elif meta['columns'][v]['type'] == 'delimited set': + w = warn + 'Stats are not valid on delimited sets!\n' + print w.format(v) + continue + elif not isinstance(meta['columns'][v]['values'], list): + parent = meta['columns'][v]['parent'].keys()[0].split('@')[-1] + items = [i.split('@')[-1] for i in meta['sets'][parent]['items']] + check_on = list(set(check_on + [items[0]])) + else: + check_on = list(set(check_on + [v])) + if any(v in meta['sets']['batches'][b]['transposed_arrays'] + for b in _batches): + on_vars += [('@', v)] + + ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) + ds.from_stack(self, dk) + if not other_source: + self._add_factor_meta(ds, v, (rescale, drop, exclude)) + + + view = qp.ViewMapper() + view.make_template('descriptives') + for stat in stats: + options['stats'] = stat + view.add_method('stat', kwargs=options) + self.aggregate(view, False, on_vars, _batches, on_vars, verbose=verbose) + + if recode: + if other_source: + raise ValueError('Cannot recode if other_source is provided.') + ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) + ds.from_stack(self, dk) + on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] + _recode_from_stat_def(ds, on_vars, rescale, drop, exclude, verbose) + + if factor_labels: + args = [rescale, drop, exclude, factor_labels] + all_batches = meta['sets']['batches'].keys() + if not _batches: _batches = all_batches + batches = [b for b in all_batches if b in _batches] + for v in check_on: + globally = False + for b in batches: + batch_me = meta['sets']['batches'][b]['meta_edits'] + values = batch_me.get(v, {}).get('values', []) + if not values: + globally = True + else: + _add_factors(v, batch_me, values, args) + if globally: + values = meta['columns'][v]['values'] + _add_factors(v, meta, values, args) + if isinstance(checking_cluster, ChainManager): + cm_meta = checking_cluster.stack['checks'].meta + values = cm_meta['columns'][v]['values'] + _add_factors(v, cm_meta, values, args) + if checking_cluster and 'mean' in stats and check_on: + + options['stats'] = 'mean' + c_view = qp.ViewMapper().make_template('descriptives') + c_view.add_method('stat_check', kwargs=options) + + views = ('stat', ['cbase', 'counts'], c_view) + checking_cluster = self._add_checking_chain(dk, checking_cluster, + 'stat_check', check_on, ['@'], views) + + return None + + @modify(to_list=['_batches']) + def add_tests(self, _batches='all', verbose=True): + """ + Apply coltests for selected batches. + + Sig. Levels are taken from ``qp.Batch`` definitions. + + Parameters + ---------- + _batches: str or list of str + Only for ``qp.Links`` that are defined in this ``qp.Batch`` + instances views are added. + + Returns + ------- + None + """ + self._remove_coltests() + + if verbose: + start = time.time() + + for dk in self.keys(): + _batches = self._check_batches(dk, _batches) + if not _batches: return None + for batch_name in _batches: + batch = self[dk].meta['sets']['batches'][batch_name] + sigpro = batch.get('sigproperties', {}) + levels = batch.get('sigproperties', batch).get('siglevels', []) + weight = batch['weights'] + x_y = batch['x_y_map'] + x_f = batch['x_filter_map'] + y_f = batch['y_filter_map'] + yks = batch['yks'] + + if levels: + vm_tests = qp.ViewMapper().make_template( + method='coltests', + iterators={'metric': ['props', 'means'], + 'mimic': sigpro.get('mimic', ['Dim']), + 'level': levels}) + vm_tests.add_method('significance', + kwargs = {'flag_bases': sigpro.get('flag_bases', [30, 100]), + 'test_total': sigpro.get('test_total', None), + 'groups': 'Tests'}) + for yy in batch['y_on_y']: + if y_f[yy]: + fy = y_f[yy].encode('utf8') + f = {fy: {fy: 0}} + else: + f = ['no_filter'] + self.add_link(filters=f, x=yks[1:], y=yks, + views=vm_tests, weights=weight) + total_len = len(x_y) + for idx, xy in enumerate(x_y, 1): + x, y = xy + if x == '@': continue + if x_f[x]: + fx = x_f[x].encode('utf8') + f = {fx: {fx: 0}} + else: + f = ['no_filter'] + self.add_link(filters=f, x=x, y=y, + views=vm_tests, weights=weight) + if verbose: + done = float(idx) / float(total_len) *100 + print '\r', + time.sleep(0.01) + print 'Batch [{}]: {} %'.format(batch_name, round(done, 1)), + sys.stdout.flush() + if verbose and levels: print '\n' + if verbose: print 'Sig-Tests:', time.time()-start + return None + + def _remove_coltests(self, props=True, means=True): + """ + Remove coltests from stack. + + Parameters + ---------- + props : bool, default=True + If True, column proportion test view will be removed from stack. + means : bool, default=True + If True, column mean test view will be removed from stack. + """ + for dk in self.keys(): + for fk in self[dk].keys(): + for xk in self[dk][fk].keys(): + for yk in self[dk][fk][xk].keys(): + for vk in self[dk][fk][xk][yk].keys(): + del_prop = props and 't.props' in vk + del_mean = means and 't.means' in vk + if del_prop or del_mean: + del self[dk][fk][xk][yk][vk] + del self[dk][fk][xk][yk][vk] + return None From 5906eea469766e9abc32e3554a361632f2e49313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 30 Oct 2018 12:37:39 +0100 Subject: [PATCH 579/733] add warning --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index ab04cb6f1..5d5a8b964 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6006,7 +6006,7 @@ def set_value_texts(self, name, renamed_vals, text_key=None, axis_edit=None): if ignore: msg = 'Warning: Cannot set new value texts... ' msg = msg + "Codes {} not found in values object of '{}'!" - warnings.warn(msg) + warnings.warn(msg.format(ignore, name)) else: msg = '{} has empty values object, allowing arbitrary values meta!' msg = msg + ' ...falling back to extend_values() now!' From 3a459e79b7e749dff6f18f66d79bbfcb63ba5ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 30 Oct 2018 13:18:47 +0100 Subject: [PATCH 580/733] encoding in rename mapper --- quantipy/core/dataset.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 5d5a8b964..db9d72eb7 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1874,10 +1874,14 @@ def _add_to_datafile_items_set(self, name): return None def _add_all_renames_to_mapper(self, mapper, old, new): - mapper['masks@{}'.format(old)] = 'masks@{}'.format(new) - mapper['columns@{}'.format(old)] = 'columns@{}'.format(new) - mapper['lib@values@{}'.format(old)] = 'lib@values@{}'.format(new) - mapper[old] = new + mapper['masks@{}'.format(old).encode('utf8')] = 'masks@{}'.format(new) + mapper['columns@{}'.format(old).encode('utf8')] = 'columns@{}'.format(new) + mapper['lib@values@{}'.format(old).encode('utf8')] = 'lib@values@{}'.format(new) + mapper[old.encode('utf8')] = new + mapper['masks@{}'.format(old).decode('utf8')] = 'masks@{}'.format(new) + mapper['columns@{}'.format(old).decode('utf8')] = 'columns@{}'.format(new) + mapper['lib@values@{}'.format(old).decode('utf8')] = 'lib@values@{}'.format(new) + mapper[old.decode('utf8')] = new return mapper @classmethod @@ -4969,7 +4973,6 @@ def rename_from_mapper(self, mapper, keep_original=False, None DataSet is modified inplace. """ - def rename_properties(mapper): """ Rename variable properties that reference other variables, i.e. @@ -5071,6 +5074,15 @@ def rename_sets(sets, mapper, keep_original): for i, item in enumerate(items): if item in mapper: items[i] = mapper[item] + data_file = [] + for i in sets['data file']['items']: + if i in mapper: + if keep_original: + data_file.append(i) + data_file.append(mapper[i]) + else: + data_file.append(i) + sets['data file']['items'] = data_file def rename_batch_properties(batches, mapper): From 68390fbb067e0f72f59200f8f3b2a7be89019ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 30 Oct 2018 15:07:57 +0100 Subject: [PATCH 581/733] undo datafile list rename --- quantipy/core/dataset.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index db9d72eb7..515c2b08c 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -5074,15 +5074,6 @@ def rename_sets(sets, mapper, keep_original): for i, item in enumerate(items): if item in mapper: items[i] = mapper[item] - data_file = [] - for i in sets['data file']['items']: - if i in mapper: - if keep_original: - data_file.append(i) - data_file.append(mapper[i]) - else: - data_file.append(i) - sets['data file']['items'] = data_file def rename_batch_properties(batches, mapper): From 71e87bd3a8d4cc4fc2c1ba0b4d8810f395b57378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 31 Oct 2018 13:01:14 +0100 Subject: [PATCH 582/733] fix filter compare --- quantipy/core/dataset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 515c2b08c..0d7288997 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3343,8 +3343,9 @@ def compare_filter(self, name1, name2): if not all(self.is_filter(f) for f in [name1] + name2): raise ValueError('Can only compare filter variables') equal = True + f0 = self.manifest_filter(name1).tolist() for f in name2: - if not all(self.manifest_filter(name1) == self.manifest_filter(f)): + if not f0 == self.manifest_filter(f).tolist(): equal = False return equal From 1499eba326d69467c919c059764c4af5fd370631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 31 Oct 2018 13:33:49 +0100 Subject: [PATCH 583/733] fix _toggle_bases in sandbox --- quantipy/sandbox/sandbox.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 39091d71c..268415408 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2750,25 +2750,22 @@ def _toggle_bases(self, keep_weighted=True): drop_rows = has_wgt_b names = ['x|f|x:||{}|cbase'.format(contents.values()[0]['weight'])] - drop_labs = [df.index[r] if not is_array else df.columns[r] - for r in drop_rows] - if is_array: - drop_labs = [df.columns[r] for r in drop_rows] - keep_rows = [x for x, y in enumerate(self._frame.columns.get_level_values(1).tolist()) - if not x in drop_rows] - else: - drop_labs = [df.index[r] for r in drop_rows] - keep_rows = [x for x, y in enumerate(self._frame.index.get_level_values(1).tolist()) - if not x in drop_rows] - for v in self.views.copy(): if v in names: del self._views[v] + df = self._frame + if is_array: - self.columns = self._slice_edited_index(self.columns, keep_rows) + cols = [col for x, col in enumerate(df.columns.tolist()) + if not x in drop_rows] + df = df.loc[:, cols] else: - self.index = self._slice_edited_index(self.index, keep_rows) + rows = [row for x, row in enumerate(df.index.tolist()) + if not x in drop_rows] + df = df.loc[rows, :] + + self._frame = df return None def _slice_edited_index(self, axis, positions): From b8e9292648937f843d104d9ab88f9cc36f2d2e42 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 31 Oct 2018 14:25:27 +0100 Subject: [PATCH 584/733] add line+column chart to draft_autochart --- quantipy/sandbox/pptx/PptxPainterClass.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index 6ffea72c3..14fbbedff 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -487,7 +487,14 @@ def draft_autochart(self, dataframe, chart_type): Returns: self.chart ------- """ - valid_chart_types = ['pie', 'bar_clustered', 'bar_stacked_100', 'bar'] + valid_chart_types = ['pie', + 'bar_clustered', + 'bar_stacked_100', + 'bar', + 'column', + 'column_clustered', + 'line', + ] # Validate the user-provided chart types. if not isinstance(chart_type, basestring): raise ValueError('The chart_type argument must be a string') @@ -502,6 +509,14 @@ def draft_autochart(self, dataframe, chart_type): draft = copy.deepcopy(self.chart_bar) if len(dataframe.columns) > 1: draft['has_legend'] = True + elif chart_type == 'column_clustered' or chart_type == 'column': + draft = copy.deepcopy(self.chart_column) + if len(dataframe.columns) > 1: + draft['has_legend'] = True + elif chart_type == 'line': + draft = copy.deepcopy(self.chart_line) + if len(dataframe.columns) > 1: + draft['has_legend'] = True elif chart_type == 'bar_stacked_100': draft = copy.deepcopy(self.chart_bar_stacked100) else: From 98d2d3de5e432f2337da2d8417a74ef0531649f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 2 Nov 2018 09:55:15 +0100 Subject: [PATCH 585/733] fix painting for array items --- quantipy/sandbox/sandbox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 268415408..eebb6bfaf 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3562,7 +3562,10 @@ def _get_text(self, value, text_key, item_text=False): if item_text and col.get('parent'): parent = col['parent'].keys()[0].split('@')[-1] items = self._meta['masks'][parent]['items'] - obj = [i['text'] for i in items if value in i['source']][0] + for i in items: + if i['source'].split('@')[-1] == value: + obj = i['text'] + break else: obj = col['text'] elif value in self._meta['masks'].keys(): From 88f44ab22d7bb82193dd2124f490fe711769c736 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 2 Nov 2018 10:57:56 +0100 Subject: [PATCH 586/733] set auto chart_type if none defined --- quantipy/sandbox/pptx/PptxChainClass.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index c610badc2..15362502c 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -656,6 +656,9 @@ def sig_test(self): @property def chart_type(self): + if self._chart_type is None: + self._chart_type = auto_charttype(self.chart_df.get('pct,net').df, self.array_style) + return self._chart_type @chart_type.setter From d7bc7472a06541a6c3b0e3440b0e085069720aa9 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 2 Nov 2018 11:02:17 +0100 Subject: [PATCH 587/733] change auto_charttype to default to bar_clustered --- quantipy/sandbox/pptx/PptxChainClass.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 15362502c..95754b0b4 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -193,9 +193,11 @@ def auto_charttype(df, array_style, max_pie_elms=MAX_PIE_ELMS): if len(df.index.get_level_values(-1)) <= max_pie_elms: if len(df.columns.get_level_values(-1)) == 1: chart_type = 'pie' - else: # Array Sum + elif array_style == 0: chart_type = 'bar_stacked_100' # TODO _auto_charttype - return 'bar_stacked' if rows not sum to 100 + else: + chart_type = 'bar_clustered' return chart_type From 41c725c64ff10cb0baa72df8826c92b46741117a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 2 Nov 2018 11:47:59 +0100 Subject: [PATCH 588/733] check filter names --- quantipy/core/batch.py | 8 ++++---- quantipy/core/dataset.py | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 0dfabc04e..80b639413 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -817,7 +817,7 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): if filter_by: f_name = title if not self.filter else '%s_%s' % (self.filter, title) - f_name = self._verify_filter_name(f_name) + f_name = self._verify_filter_name(f_name, number=True) logic = {'label': title, 'logic': filter_by} if self.filter: suffix = f_name[len(self.filter)+1:] @@ -1092,17 +1092,17 @@ def _map_y_on_y_filter(self): elif isinstance(y_f, basestring): f = y_f else: - f = self._verify_filter_name(y_on_y) + f = self._verify_filter_name(y_on_y, number=True) self.add_filter_var(f, logic) elif ext_rep == 'extend': if not y_f: f = self.filter elif not self.filter: - f = self._verify_filter_name(y_on_y) + f = self._verify_filter_name(y_on_y, number=True) self.add_filter_var(f, logic) else: f = '{}_{}'.format(self.filter, y_on_y) - f = self._verify_filter_name(f) + f = self._verify_filter_name(f, number=True) suf = f[len(self.filter)+1:] self.extend_filter_var(self.filter, logic, suf) self.y_filter_map[y_on_y] = f diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 0d7288997..8f2eea924 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3198,7 +3198,7 @@ def add_filter_var(self, name, logic, overwrite=False): overwrite: bool, default False Overwrite an already existing filter-variable. """ - name = name.encode('utf8').replace(' ', '_').replace('~', '_') + name = self._verify_filter_name(name, None) if name in self: if overwrite and not self.is_filter(name): msg = "Cannot add filter-variable '{}', a non-filter" @@ -3243,9 +3243,9 @@ def extend_filter_var(self, name, logic, extend_as=None): """ if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) - name = name.encode('utf8').replace(' ', '_').replace('~', '_') + name = self._verify_filter_name(name, None) if extend_as: - extend_as = extend_as.encode('utf8').replace(' ', '_').replace('~', '_') + extend_as = self._verify_filter_name(extend_as, None) f_name = '{}_{}'.format(name, extend_as) if f_name in self: msg = "Please change 'extend_as': '{}' is already in dataset." @@ -3288,10 +3288,14 @@ def _transform_filter_logics(self, logic, start): values.append(val) return values - def _verify_filter_name(self, name): - f = '{}_f'.format(name) - f = f.encode('utf8').replace(' ', '_').replace('~', '_') - f = self.enumerator(f) + def _verify_filter_name(self, name, suf='f', number=False): + f = '{}_{}'.format(name, suf) if suf else name + f = f.encode('utf8') + repl = [(' ', '_'), ('~', '_'), ('(', ''), (')', '')] + for r in repl: + f = f.replace(r[0], r[1]) + if number: + f = self.enumerator(f) return f @modify(to_list=['values']) @@ -3299,6 +3303,7 @@ def reduce_filter_var(self, name, values): """ Remove values from filter-variables and recalculate the filter. """ + name = self._verify_filter_name(name, None) if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) if 0 in values: @@ -3322,7 +3327,7 @@ def manifest_filter(self, name): if not name: return self._data.index else: - name = name.encode('utf8').replace(' ', '_').replace('~', '_') + name = self._verify_filter_name(name, None) if not self.is_filter(name): raise KeyError('{} is no valid filter-variable.'.format(name)) return self.take({name: 0}) From e5002766968de98567e5353dc7e24dfd1b722201 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 5 Nov 2018 11:22:20 +0100 Subject: [PATCH 589/733] property name must be _variables for loading/saving batches in dataset meta --- quantipy/core/batch.py | 4 ++-- quantipy/core/dataset.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 80b639413..b071729a6 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -181,7 +181,7 @@ def _update(self): self._map_y_on_y_filter() self._samplesize_from_batch_filter() attrs = self.__dict__ - for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', + for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', @@ -198,7 +198,7 @@ def _load_batch(self): Fill batch attributes with information from meta. """ bdefs = self._meta['sets']['batches'][self.name] - for attr in ['xks', 'yks', 'variables', 'filter', 'filter_names', + for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', 'extended_yks_global', 'extended_yks_per_x', diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 8f2eea924..f07de28ce 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -1025,7 +1025,7 @@ def _apply_edits_rules(ds, name, b_meta): adds = batch['additions'] if additions in ['full', 'variables'] else [] for b_name, ba in batches.items(): if not b_name in [batch_name] + adds: continue - variables += ba['xks'] + ba['yks'] + ba['variables'] + variables += ba['xks'] + ba['yks'] + ba['_variables'] for oe in ba['verbatims']: variables += oe['columns'] variables += ba['weights'] From 60179434deaeb4d472480b950b47999eca0e7a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 6 Nov 2018 10:18:12 +0100 Subject: [PATCH 590/733] verify filter name in batch --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 80b639413..511bf59cb 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -740,7 +740,7 @@ def add_filter(self, filter_name, filter_logic=None, overwrite=False): ------- None """ - name = filter_name.encode('utf8').replace(' ', '_').replace('~', '_') + name = self._verify_filter_name(filter_name, None) if self.is_filter(name): if not (filter_logic is None or overwrite): raise ValueError("'{}' is already a filter-variable. Cannot " From 6c89174575bdb858db3fa06ea709c0c00f698e35 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 6 Nov 2018 14:40:04 +0100 Subject: [PATCH 591/733] ChainManager now handles sig test result letters in a different way --- quantipy/sandbox/sandbox.py | 55 +++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 39091d71c..8f61cd812 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3039,7 +3039,7 @@ def _temp_nest_index(df): return df, flat_cols @staticmethod - def _replace_test_results(df, replacement_map): + def _replace_test_results(df, replacement_map, char_repr): """ Swap all digit-based results with letters referencing the column header. @@ -3055,16 +3055,30 @@ def _replace_test_results(df, replacement_map): values = value_df.replace(np.NaN, '-').values.tolist() r = replacement_map[col] new_values = [] + case = None for v in values: if isinstance(v[0], (str, unicode)): - for number, letter in sorted(r.items(), reverse=True): - v = [char.replace(str(number), letter) + if char_repr == 'upper': + case = 'up' + elif char_repr == 'lower': + case = 'low' + elif char_repr == 'alternate': + if case == 'up': + case = 'low' + else: + case = 'up' + for no, l in sorted(r.items(), reverse=True): + v = [char.replace(str(no), l if case == 'up' else l.lower()) if isinstance(char, (str, unicode)) else char for char in v] + new_values.append(v) else: new_values.append(v) + + part_df = pd.DataFrame(new_values) + all_dfs.append(part_df) letter_df = pd.concat(all_dfs, axis=1) # Clean it up @@ -3097,7 +3111,12 @@ def _any_tests(self): vms = [v.split('|')[1] for v in self._views.keys()] return any('t.' in v for v in vms) - def transform_tests(self): + def _no_of_tests(self): + levels = [v.split('|')[1].split('.')[-1] for v in self._views.keys()] + return len(set(levels)) + + + def transform_tests(self, char_repr='upper'): """ Transform column-wise digit-based test representation to letters. @@ -3112,6 +3131,9 @@ def transform_tests(self): number_codes = df.columns.get_level_values(-1).tolist() number_header_row = copy.copy(df.columns) + if self._no_of_tests() != 2 and char_repr == 'alternate': + char_repr == 'upper' + has_total = '@' in self._y_keys if self._nested_y: df, questions = self._temp_nest_index(df) @@ -3126,11 +3148,6 @@ def transform_tests(self): (vals, column_letters)) df.columns = mi - # Old version: fails because undwerlying frozenllist dtype is int8 - # -------------------------------------------------------------------- - # df.columns.set_levels(levels=column_letters, level=1, inplace=True) - # df.columns.set_labels(labels=xrange(0, len(column_letters)), level=1, - # inplace=True) self.sig_test_letters = df.columns.get_level_values(1).tolist() # Build the replacements dict and build list of unique column indices test_dict = OrderedDict() @@ -3142,9 +3159,8 @@ def transform_tests(self): if not question in test_dict: test_dict[question] = {} number = all_num[num_idx] letter = col[1] - test_dict[question][number] = letter - # Do the replacements... - letter_df = self._replace_test_results(df, test_dict) + test_dict[question][number] = letter + letter_df = self._replace_test_results(df, test_dict, char_repr) # Re-apply indexing & finalize the new crossbreak column header letter_df.index = df.index letter_df.columns = number_header_row @@ -3233,9 +3249,10 @@ def _finish_text_key(self, text_key, text_loc_x, text_loc_y): return text_keys def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, - axes=None, view_level=False, transform_tests='cells', - add_base_texts='simple', totalize=False, sep=None, na_rep=None, - transform_column_names=None, exclude_mask_text=False): + axes=None, view_level=False, transform_tests='upper', + add_test_ids=True, add_base_texts='simple', totalize=False, + sep=None, na_rep=None, transform_column_names=None, + exclude_mask_text=False): """ Apply labels, sig. testing conversion and other post-processing to the ``Chain.dataframe`` property. @@ -3257,7 +3274,9 @@ def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, Text view_level : bool, default False Text - transform_tests : {False, 'full', 'cells'}, default 'cells' + transform_tests : {False, 'upper', 'lower', 'alternate'}, default 'upper' + Text + add_test_ids : bool, default True Text add_base_texts : {False, 'all', 'simple', 'simple-no-items'}, default 'simple' Whether or not to include existing ``.base_descriptions`` str @@ -3284,7 +3303,7 @@ def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, self._paint_structure(text_key, sep=sep, na_rep=na_rep) else: self.totalize = totalize - if transform_tests: self.transform_tests() + if transform_tests: self.transform_tests(transform_tests) # Remove any letter header row from transformed tests... if self.sig_test_letters: self._remove_letter_header() @@ -3295,7 +3314,7 @@ def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, self._paint(text_keys, display, axes, add_base_texts, transform_column_names, exclude_mask_text) # Re-build the full column index (labels + letter row) - if self.sig_test_letters and transform_tests == 'full': + if self.sig_test_letters and add_test_ids: self._frame = self._apply_letter_header(self._frame) if view_level: self._add_view_level() From fcac1c8799cb4d912feb32ab5dcdb51960f20f55 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 8 Nov 2018 14:05:37 +0100 Subject: [PATCH 592/733] new option to show sig level on the painted row index, transform_tests, needs cleanup --- quantipy/sandbox/sandbox.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 8f61cd812..a6d1ce3b9 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3115,8 +3115,18 @@ def _no_of_tests(self): levels = [v.split('|')[1].split('.')[-1] for v in self._views.keys()] return len(set(levels)) + def _siglevel_on_row(self): + """ + """ + vpr = self._views_per_rows() + tests = [(no, v) for no, v in enumerate(vpr) + if v.split('|')[1].startswith('t.')] + s = [(t[0], + float(int(t[1].split('|')[1].split('.')[3].split('+')[0]))/100.0) + for t in tests] + return s - def transform_tests(self, char_repr='upper'): + def transform_tests(self, char_repr='upper', display_level=True): """ Transform column-wise digit-based test representation to letters. @@ -3162,7 +3172,18 @@ def transform_tests(self, char_repr='upper'): test_dict[question][number] = letter letter_df = self._replace_test_results(df, test_dict, char_repr) # Re-apply indexing & finalize the new crossbreak column header - letter_df.index = df.index + if display_level: + levels = self._siglevel_on_row() + index = df.index.get_level_values(1).tolist() + for i, l in levels: + index[i] = '#Level: {}'.format(l) + l0 = df.index.get_level_values(0).tolist()[0] + tuples = [(l0, i) for i in index] + index = pd.MultiIndex.from_tuples( + tuples, names=['Question', 'Values']) + letter_df.index = index + else: + letter_df.index = df.index letter_df.columns = number_header_row letter_df = self._apply_letter_header(letter_df) self._frame = letter_df @@ -3478,6 +3499,8 @@ def _get_level_1(self, levels, text_keys, display, axis, bases): level_1_text.append(value) elif str(value) == '': level_1_text.append(value) + elif str(value).startswith('#Level: '): + level_1_text.append(value.replace('#Level: ', '')) else: translate = self._transl[self._transl.keys()[0]].keys() if value in self._text_map.keys() and value not in translate: From 4c3f4d82be11451f8fb3b898898f4817ec4232e9 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 9 Nov 2018 11:05:11 +0100 Subject: [PATCH 593/733] bugfix --- quantipy/sandbox/excel.py | 1 + quantipy/sandbox/sandbox.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/excel.py b/quantipy/sandbox/excel.py index 6decb3b90..b047346ce 100644 --- a/quantipy/sandbox/excel.py +++ b/quantipy/sandbox/excel.py @@ -645,6 +645,7 @@ def write_chains(self): if arrow_descriptions: arrow_format = _Format(**{'font_color': self.arrow_color_high}) arrow_format = self.excel._add_format(**arrow_format) + print cds write_rich_string(self._row + 2, self._column + 1, arrow_format, self.arrow_rep_high, format_, cds[1], format_) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 39091d71c..31be8a989 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1999,15 +1999,15 @@ def cell_items(self): non_freqs = ('d.', 't.') c = any(v.split('|')[3] == '' and not v.split('|')[1].startswith(non_freqs) and - not v.split('|')[-1] == 'cbase' + not v.split('|')[-1].startswith('cbase') for v in check_views) col_pct = any(v.split('|')[3] == 'y' and not v.split('|')[1].startswith(non_freqs) and - not v.split('|')[-1] == 'cbase' + not v.split('|')[-1].startswith('cbase') for v in check_views) row_pct = any(v.split('|')[3] == 'x' and not v.split('|')[1].startswith(non_freqs) and - not v.split('|')[-1] == 'cbase' + not v.split('|')[-1].startswith('cbase') for v in check_views) c_colpct = c and col_pct c_rowpct = c and row_pct From 2cf52a27da601f1396609a4fd9edb9167fea0bc8 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 9 Nov 2018 11:05:49 +0100 Subject: [PATCH 594/733] remove debug print --- quantipy/sandbox/excel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/quantipy/sandbox/excel.py b/quantipy/sandbox/excel.py index b047346ce..6decb3b90 100644 --- a/quantipy/sandbox/excel.py +++ b/quantipy/sandbox/excel.py @@ -645,7 +645,6 @@ def write_chains(self): if arrow_descriptions: arrow_format = _Format(**{'font_color': self.arrow_color_high}) arrow_format = self.excel._add_format(**arrow_format) - print cds write_rich_string(self._row + 2, self._column + 1, arrow_format, self.arrow_rep_high, format_, cds[1], format_) From f64aa1955ec81feecb5ee7ace4549a87207b44e9 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 9 Nov 2018 12:04:48 +0100 Subject: [PATCH 595/733] some bugfixes for _toggle_bases --- quantipy/sandbox/sandbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index e1d5dbbc6..ad87c9471 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2766,6 +2766,8 @@ def _toggle_bases(self, keep_weighted=True): df = df.loc[rows, :] self._frame = df + self._index = df.index + self._columns = df.columns return None def _slice_edited_index(self, axis, positions): @@ -3629,7 +3631,6 @@ def toggle_labels(self): attrs = ['index', 'columns'] if self.structure is not None: attrs.append('_frame_values') - for attr in attrs: vals = attr[6:] if attr.startswith('_frame') else attr frame_val = getattr(self._frame, vals) @@ -3639,7 +3640,7 @@ def toggle_labels(self): if self.structure is not None: values = self._frame.values self._frame.loc[:, :] = self.frame_values - self.fram_values = values + self.frame_values = values return self From 3f463ef59adfe8899e9f79ac60620105debc9b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 13 Nov 2018 13:07:41 +0100 Subject: [PATCH 596/733] check filter names: remove "&" --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f07de28ce..b99067b6b 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3291,7 +3291,7 @@ def _transform_filter_logics(self, logic, start): def _verify_filter_name(self, name, suf='f', number=False): f = '{}_{}'.format(name, suf) if suf else name f = f.encode('utf8') - repl = [(' ', '_'), ('~', '_'), ('(', ''), (')', '')] + repl = [(' ', '_'), ('~', '_'), ('(', ''), (')', ''), ('&', '_')] for r in repl: f = f.replace(r[0], r[1]) if number: From 335adce2166c71369ca829f439a783dc171a8b00 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 13 Nov 2018 14:09:19 +0100 Subject: [PATCH 597/733] adding parameter --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index a6d1ce3b9..da9065ee9 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3270,7 +3270,7 @@ def _finish_text_key(self, text_key, text_loc_x, text_loc_y): return text_keys def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, - axes=None, view_level=False, transform_tests='upper', + axes=None, view_level=False, transform_tests='upper', display_level=True, add_test_ids=True, add_base_texts='simple', totalize=False, sep=None, na_rep=None, transform_column_names=None, exclude_mask_text=False): From 8187f8604e7e7ece7f37081e4246ea29f19672b7 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 14 Nov 2018 10:17:28 +0100 Subject: [PATCH 598/733] adding parameter for display_level --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index da9065ee9..7a714611c 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3324,7 +3324,7 @@ def paint(self, text_key=None, text_loc_x=None, text_loc_y=None, display=None, self._paint_structure(text_key, sep=sep, na_rep=na_rep) else: self.totalize = totalize - if transform_tests: self.transform_tests(transform_tests) + if transform_tests: self.transform_tests(transform_tests, display_level) # Remove any letter header row from transformed tests... if self.sig_test_letters: self._remove_letter_header() From bc2f50e443c85c146f4746ec13582c6b657af81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 14 Nov 2018 14:05:27 +0100 Subject: [PATCH 599/733] fix for new filter in batch.hide_empty --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index b071729a6..b6c90d9a3 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -552,7 +552,7 @@ def hide_empty(self, xks=True, summaries=True): if self.filter == 'no_filter': cond = None else: - cond = self.filter.values()[0] + cond = {self.filter: 0} removed_sum = [] for x in self.xks[:]: if self.is_array(x): From 5a13d5b7630765b5f79470be7eb160ddc95cbf22 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 16 Nov 2018 08:57:30 +0100 Subject: [PATCH 600/733] bugfix for alternating array items that miss labels --- quantipy/sandbox/sandbox.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index ad87c9471..e22d0f28f 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2710,11 +2710,20 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, self._frame = pd.concat(self._pad(x_frames), axis=self.axis) if self._group_style == 'reduced' and self.array_style >- 1: - if not any(len(v) == 2 and any(view.split('|')[1].startswith('t.') - for view in v) for v in self._given_views): - self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) - elif any(len(v) == 3 for v in self._given_views): + # OLD CHECK: + # ------------------------------------------------------------ + # if not any(len(v) == 2 and any(view.split('|')[1].startswith('t.') + # for view in v) for v in self._given_views): + + test_given_views = [v if isinstance(v, (tuple, list)) else [v] for v in self._given_views] + cond1 = any(len(v) >= 2 for v in test_given_views) + cond2 = any(view.split('|')[1].startswith('t.') for view in v for v in test_given_views) + if not(cond1 and cond2): self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) + # CONTINUED: + # ------------------------------------------------------------ + # elif any(len(v) == 3 for v in self._given_views): + # self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) if self.axis == 1: self.views = found[-1] From 9fe84214b294f9304cf0a77e54a759419438fffe Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 20 Nov 2018 18:05:18 +0100 Subject: [PATCH 601/733] stupid list compr. error fixed... --- quantipy/sandbox/sandbox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index e22d0f28f..4661f808f 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2717,7 +2717,10 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, test_given_views = [v if isinstance(v, (tuple, list)) else [v] for v in self._given_views] cond1 = any(len(v) >= 2 for v in test_given_views) - cond2 = any(view.split('|')[1].startswith('t.') for view in v for v in test_given_views) + cond2 = False + for tgv in test_given_views: + for view in tgv: + if view.split('|')[1].startswith('t.'): cond2 = True if not(cond1 and cond2): self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) # CONTINUED: From c4eec0a8c4f8b34a7802202bc7dc15386358bf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 21 Nov 2018 15:05:41 +0100 Subject: [PATCH 602/733] fix for renaming_properties for array items --- quantipy/core/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index b99067b6b..9ef686373 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4988,6 +4988,7 @@ def rename_properties(mapper): stat_recs = self._stat_view_recodes() all_recs = set([r for r in net_recs + stat_recs if r in mapper]) for rec in all_recs: + if self._is_array_item(rec): continue rn = self.get_property(rec, 'recoded_net') if rn: self._set_property(rec, 'recoded_net', mapper[rn]) rs = self.get_property(rec, 'recoded_stat') From 81b49b159f8f1c4d26d5e9d52b86bf98b5764803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 22 Nov 2018 10:55:11 +0100 Subject: [PATCH 603/733] filter fix in batch + adjust gitignore --- .gitignore | 1 + quantipy/core/batch.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e188fdab4..945fc4981 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /tests/StackName.stack /tests/ChainName.chain /tests/ClusterName.cluster +dist/ .coverage htmlcov/ diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 3bf6156bc..7523391d9 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -38,7 +38,7 @@ def edit(*args, **kwargs): kwargs['weight'] = self.weights[0] if len(args) < 4 and not 'condition' in kwargs: - if not self.filter == 'no_filter': + if self.filter: kwargs['condition'] = self.filter.values()[0] # args/ kwargs for sorting elif dataset_func.func_name == 'sorting': @@ -549,10 +549,7 @@ def hide_empty(self, xks=True, summaries=True): ------- None """ - if self.filter == 'no_filter': - cond = None - else: - cond = self.filter.values()[0] + cond = {0: self.filter} if self.filter else None removed_sum = [] for x in self.xks[:]: if self.is_array(x): From db36f2f017a7f5c4ac6f4f016c728efcc5f71f9a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 23 Nov 2018 08:54:13 +0100 Subject: [PATCH 604/733] checkpoint --- quantipy/sandbox/sandbox.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 4661f808f..69178ad8b 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2670,7 +2670,16 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, self._has_rules = ['x', 'y'] else: self._has_rules = rules + + use_views = views[:] + for first in self.axes[0]: + for second in self.axes[1]: + link = self._get_link(data_key, filter_key, first, second) + for v in use_views: + if v not in link: + use_views.remove(v) + for first in self.axes[0]: found = [] x_frames = [] @@ -2683,10 +2692,9 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, if link is None: continue - if prioritize: link = self._drop_substituted_views(link) found_views, y_frames = self._concat_views( - link, views, rules_weight) + link, use_views, rules_weight) found.append(found_views) try: @@ -2709,6 +2717,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, self._frame = pd.concat(self._pad(x_frames), axis=self.axis) + if self._group_style == 'reduced' and self.array_style >- 1: # OLD CHECK: # ------------------------------------------------------------ From 447ec11470c708ecc3aa66a7b9bfafd788a234f4 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 23 Nov 2018 18:10:16 +0100 Subject: [PATCH 605/733] testing: how to modify excel.py to stop alternate_bg for sig level rows (INDIA) --- quantipy/sandbox/excel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/excel.py b/quantipy/sandbox/excel.py index 6decb3b90..5791fbad2 100644 --- a/quantipy/sandbox/excel.py +++ b/quantipy/sandbox/excel.py @@ -1121,7 +1121,6 @@ def _write_rows(self): level_1, values, contents = (levels(1).values, self.values, self.contents) - row_max = max(contents.keys()) flat = np.c_[level_1.T, values].flat rel_x, rel_y = flat.coords @@ -1150,7 +1149,6 @@ def _write_rows(self): name = 'counts' else: name = self._row_format_name(**x_contents) - if rel_y == 0: if data == '': view_border = False @@ -1264,6 +1262,7 @@ def _alternate_bg(self, name, bg): if ((is_freq_test and not_net_sum) or freq_view_group) or \ (not is_mean and self.chain.array_style == 0): return not bg, bg + return self.sheet.alternate_bg, True def _row_format_name(self, **contents): @@ -1390,7 +1389,8 @@ def _get_dummies(self, index, values): while True: try: ndx, next_ = next(it) - if next_ == '': + sig_level_row = self.contents[ndx]['siglevel'] and next_ != '' + if next_ == '' or sig_level_row: if not group: group = data if self._is('test', **self.contents[ndx]): From 9e2f615016de7b56a905114850c2772eb71b149a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 26 Nov 2018 09:40:18 +0100 Subject: [PATCH 606/733] fixing formats for INDIA sigtesting settings --- quantipy/sandbox/excel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/excel.py b/quantipy/sandbox/excel.py index 5791fbad2..2503444b5 100644 --- a/quantipy/sandbox/excel.py +++ b/quantipy/sandbox/excel.py @@ -1145,12 +1145,15 @@ def _write_rows(self): else: x_contents = contents[rel_x] + if rel_y == 0 and self.chain.array_style == 0: name = 'counts' else: name = self._row_format_name(**x_contents) + if rel_y == 0: - if data == '': + sig_level_row = data != '' and name in ['propstest', 'meanstest'] + if data == '' or sig_level_row: view_border = False else: view_border = True From cf1504564012fe9dc2f74a2964de202a4a8deb55 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 26 Nov 2018 10:27:50 +0100 Subject: [PATCH 607/733] bugfix for wrong detection of multiple sig levels... --- quantipy/sandbox/sandbox.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 7a714611c..84f2be66b 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -3047,6 +3047,7 @@ def _replace_test_results(df, replacement_map, char_repr): and columns. """ all_dfs = [] + ignore = False for col in replacement_map.keys(): target_col = df.columns[0] if col == '@' else col value_df = df[[target_col]].copy() @@ -3075,10 +3076,7 @@ def _replace_test_results(df, replacement_map, char_repr): new_values.append(v) else: new_values.append(v) - - part_df = pd.DataFrame(new_values) - all_dfs.append(part_df) letter_df = pd.concat(all_dfs, axis=1) # Clean it up @@ -3112,7 +3110,9 @@ def _any_tests(self): return any('t.' in v for v in vms) def _no_of_tests(self): - levels = [v.split('|')[1].split('.')[-1] for v in self._views.keys()] + tests = [v for v in self._views.keys() + if v.split('|')[1].startswith('t.')] + levels = [v.split('|')[1].split('.')[-1] for v in tests] return len(set(levels)) def _siglevel_on_row(self): @@ -3140,10 +3140,8 @@ def transform_tests(self, char_repr='upper', display_level=True): df = self.dataframe.copy() number_codes = df.columns.get_level_values(-1).tolist() number_header_row = copy.copy(df.columns) - if self._no_of_tests() != 2 and char_repr == 'alternate': - char_repr == 'upper' - + char_repr = 'upper' has_total = '@' in self._y_keys if self._nested_y: df, questions = self._temp_nest_index(df) @@ -3169,7 +3167,7 @@ def transform_tests(self, char_repr='upper', display_level=True): if not question in test_dict: test_dict[question] = {} number = all_num[num_idx] letter = col[1] - test_dict[question][number] = letter + test_dict[question][number] = letter letter_df = self._replace_test_results(df, test_dict, char_repr) # Re-apply indexing & finalize the new crossbreak column header if display_level: From 6142cd5de71b847ca4cbb38b00a10e5f9482202b Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 27 Nov 2018 11:11:18 +0100 Subject: [PATCH 608/733] removing code --- quantipy/sandbox/sandbox.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 69178ad8b..508323032 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2671,14 +2671,14 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, else: self._has_rules = rules - use_views = views[:] - for first in self.axes[0]: - for second in self.axes[1]: - link = self._get_link(data_key, filter_key, first, second) - - for v in use_views: - if v not in link: - use_views.remove(v) + # use_views = views[:] + # for first in self.axes[0]: + # for second in self.axes[1]: + # link = self._get_link(data_key, filter_key, first, second) + + # for v in use_views: + # if v not in link: + # use_views.remove(v) for first in self.axes[0]: found = [] @@ -2694,7 +2694,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, continue if prioritize: link = self._drop_substituted_views(link) found_views, y_frames = self._concat_views( - link, use_views, rules_weight) + link, views, rules_weight) found.append(found_views) try: From ed8356aba7329f4a1b7f8125f030c141b5318846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 28 Nov 2018 14:04:00 +0100 Subject: [PATCH 609/733] fix merge --- quantipy/core/tools/dp/prep.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quantipy/core/tools/dp/prep.py b/quantipy/core/tools/dp/prep.py index 84b479248..e867471b0 100644 --- a/quantipy/core/tools/dp/prep.py +++ b/quantipy/core/tools/dp/prep.py @@ -1262,6 +1262,9 @@ def merge_meta(meta_left, meta_right, from_set, overwrite_text=False, for col_name in cols: if verbose: print '...', col_name + # store properties + props = copy.deepcopy( + meta_right['columns'][col_name].get('properties', {})) # emulate the right meta right_column = emulate_meta( meta_right, @@ -1283,7 +1286,10 @@ def merge_meta(meta_left, meta_right, from_set, overwrite_text=False, right_column['properties']['merged'] = True else: right_column['properties'] = {'merged': True} + meta_left['columns'][col_name] = right_column + if 'properties' in meta_left['columns'][col_name]: + meta_left['columns'][col_name]['properties'].update(props) if col_name in mask_items: meta_left['columns'][col_name]['values'] = mask_items[col_name] From 1386a28559bb0aaaf3baf32580849f09bf917989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 28 Nov 2018 17:18:36 +0100 Subject: [PATCH 610/733] fix cell contents for expandedn nets --- quantipy/sandbox/sandbox.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 74ba32acd..94045e226 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2033,6 +2033,8 @@ def _ci_simple(self): ci = [] if self.views: for v in self.views: + if 'significance' in v: + continue if ']*:' in v: if v.split('|')[3] == '': if 'N' not in ci: @@ -2670,7 +2672,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, self._has_rules = ['x', 'y'] else: self._has_rules = rules - + # use_views = views[:] # for first in self.axes[0]: # for second in self.axes[1]: @@ -2679,7 +2681,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, # for v in use_views: # if v not in link: # use_views.remove(v) - + for first in self.axes[0]: found = [] x_frames = [] @@ -2723,7 +2725,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, # ------------------------------------------------------------ # if not any(len(v) == 2 and any(view.split('|')[1].startswith('t.') # for view in v) for v in self._given_views): - + test_given_views = [v if isinstance(v, (tuple, list)) else [v] for v in self._given_views] cond1 = any(len(v) >= 2 for v in test_given_views) cond2 = False @@ -3084,7 +3086,7 @@ def _replace_test_results(df, replacement_map, char_repr): elif char_repr == 'lower': case = 'low' elif char_repr == 'alternate': - if case == 'up': + if case == 'up': case = 'low' else: case = 'up' @@ -3092,7 +3094,7 @@ def _replace_test_results(df, replacement_map, char_repr): v = [char.replace(str(no), l if case == 'up' else l.lower()) if isinstance(char, (str, unicode)) else char for char in v] - + new_values.append(v) else: new_values.append(v) @@ -3141,7 +3143,7 @@ def _siglevel_on_row(self): vpr = self._views_per_rows() tests = [(no, v) for no, v in enumerate(vpr) if v.split('|')[1].startswith('t.')] - s = [(t[0], + s = [(t[0], float(int(t[1].split('|')[1].split('.')[3].split('+')[0]))/100.0) for t in tests] return s From 7f34885e31ca38d1bcb33659cf49dda34f18cf41 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 14 Dec 2018 10:33:09 +0100 Subject: [PATCH 611/733] checkpoint --- quantipy/sandbox/sandbox.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 74ba32acd..1d79f2776 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1786,6 +1786,7 @@ class _TransformedChainDF(object): """ def __init__(self, chain): c = chain.clone() + self.org_views = c.views self.df = c._frame self._org_idx = self.df.index self._edit_idx = range(0, len(self._org_idx)) @@ -1800,6 +1801,7 @@ def __init__(self, chain): zip(self._edit_col, self._org_col.get_level_values(1))} self.df.columns = self._edit_col + self.array_mi = c._array_style == 0 return None def _updated_index_tuples(self, axis): @@ -1813,8 +1815,20 @@ def _updated_index_tuples(self, axis): current = self.df.index.values.tolist() mapped = self._idx_valmap org_tuples = self._org_idx.tolist() + merged = [mapped[val] if val in mapped else val for val in current] new_tuples = [] + + + + # inserts_by_position = [merged.index(val) for val in merged + # if not val in mapped.values()] + # new_views = [] + # for name, no in self.org_views.items(): + # e = [name] * no + # new_views.extend(e) + + i = d = 0 for merged_val in merged: idx = i-d if i-d != len(org_tuples) else i-d-1 @@ -1829,11 +1843,15 @@ def _updated_index_tuples(self, axis): def _reindex(self): """ """ - names = ['Question', 'Values'] + y_names = ['Question', 'Values'] + if not self.array_mi: + x_names = y_names + else: + x_names = ['Array', 'Questions'] tuples = self._updated_index_tuples(axis=0) - self.df.index = pd.MultiIndex.from_tuples(tuples, names=names) + self.df.index = pd.MultiIndex.from_tuples(tuples, names=x_names) tuples = self._updated_index_tuples(axis=1) - self.df.columns = pd.MultiIndex.from_tuples(tuples, names=names) + self.df.columns = pd.MultiIndex.from_tuples(tuples, names=y_names) return None def export(self): From 507c8e58a917c5cdc7e7ea608b7b450de9e40b66 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 17 Dec 2018 15:21:11 +0100 Subject: [PATCH 612/733] export/assign with viewlike calc on the side axis --- quantipy/sandbox/excel.py | 2 + quantipy/sandbox/sandbox.py | 88 +++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/quantipy/sandbox/excel.py b/quantipy/sandbox/excel.py index 2503444b5..04088a1df 100644 --- a/quantipy/sandbox/excel.py +++ b/quantipy/sandbox/excel.py @@ -1346,6 +1346,8 @@ def _row_format_name(self, **contents): return 'sem' elif contents['is_percentile']: return contents['stat'] + elif contents['is_viewlike']: + return 'counts' def _format_x(self, name, rel_x, rel_y, row_max, dummy, bg, view_border, border_from, **kwargs): diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 1d79f2776..93ec5e092 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1804,6 +1804,21 @@ def __init__(self, chain): self.array_mi = c._array_style == 0 return None + def _insert_viewlikes(self, new_index_flat, org_index_mapped): + inserts = [new_index_flat.index(val) for val in new_index_flat + if not val in org_index_mapped.values()] + flatviews = [] + for name, no in self.org_views.items(): + e = [name] * no + flatviews.extend(e) + for vno, i in enumerate(inserts): + flatviews.insert(i, '__viewlike__{}'.format(vno)) + new_views = OrderedDict() + no_of_views = Counter(flatviews) + for fv in flatviews: + if not fv in new_views: new_views[fv] = no_of_views[fv] + return new_views + def _updated_index_tuples(self, axis): """ """ @@ -1814,22 +1829,16 @@ def _updated_index_tuples(self, axis): else: current = self.df.index.values.tolist() mapped = self._idx_valmap - org_tuples = self._org_idx.tolist() - + org_tuples = self._org_idx.tolist() merged = [mapped[val] if val in mapped else val for val in current] - new_tuples = [] - - - - # inserts_by_position = [merged.index(val) for val in merged - # if not val in mapped.values()] - # new_views = [] - # for name, no in self.org_views.items(): - # e = [name] * no - # new_views.extend(e) - - + # ================================================================ + if (self.array_mi and axis == 1) or axis == 0: + self._transf_views = self._insert_viewlikes(merged, mapped) + else: + self._transf_views = self._org_views + # ================================================================ i = d = 0 + new_tuples = [] for merged_val in merged: idx = i-d if i-d != len(org_tuples) else i-d-1 if org_tuples[idx][1] == merged_val: @@ -1866,6 +1875,7 @@ def assign(self, transformed_chain_df): raise ValueError("Must pass an exported ``Chain`` instance!") transformed_chain_df._reindex() self._frame = transformed_chain_df.df + self.views = transformed_chain_df._transf_views return None def __str__(self): @@ -2013,7 +2023,14 @@ def sig_levels(self): def cell_items(self): if self.views: compl_views = [v for v in self.views if ']*:' in v] - check_views = compl_views or self.views + check_views = compl_views[:] or self.views.copy() + for v in check_views: + if v.startswith('__viewlike__'): + if compl_views: + check_views.remove(v) + else: + del check_views[v] + non_freqs = ('d.', 't.') c = any(v.split('|')[3] == '' and not v.split('|')[1].startswith(non_freqs) and @@ -2092,10 +2109,9 @@ def contents(self): for row, idx in enumerate(self._views_per_rows()): if nested: for i, v in idx.items(): - if v: - contents[row][i] = self._add_contents(v.split('|')) + contents[row][i] = self._add_contents(v) else: - contents[row] = self._add_contents(idx.split('|')) + contents[row] = self._add_contents(idx) return contents @property @@ -2256,15 +2272,20 @@ def _views_per_rows(self): metrics = [] ci = self.cell_items for v in self.views.keys(): - parts = v.split('|') - is_completed = ']*:' in v - if not self._is_c_pct(parts): - counts.extend([v]*self.views[v]) - if self._is_r_pct(parts): - rowpcts.extend([v]*self.views[v]) - if (self._is_c_pct(parts) or self._is_base(parts) or - self._is_stat(parts)): - colpcts.extend([v]*self.views[v]) + if not v.startswith('__viewlike__'): + parts = v.split('|') + is_completed = ']*:' in v + if not self._is_c_pct(parts): + counts.extend([v]*self.views[v]) + if self._is_r_pct(parts): + rowpcts.extend([v]*self.views[v]) + if (self._is_c_pct(parts) or self._is_base(parts) or + self._is_stat(parts)): + colpcts.extend([v]*self.views[v]) + else: + counts = counts + ['__viewlike__'] + colpcts = colpcts + ['__viewlike__'] + rowpcts = rowpcts + ['__viewlike__'] # else: # if ci == 'counts_colpct' and self.grouping: # if not self._is_counts(parts): @@ -2316,7 +2337,15 @@ def _valid_views(self, flat=False): return clean_view_list - def _add_contents(self, parts): + def _add_contents(self, viewelement): + """ + """ + if viewelement.startswith('__viewlike__'): + parts = '|||||' + viewlike = True + else: + parts = viewelement.split('|') + viewlike = False return dict(is_default=self._is_default(parts), is_c_base=self._is_c_base(parts), is_r_base=self._is_r_base(parts), @@ -2348,7 +2377,8 @@ def _add_contents(self, parts): weight=self._weight(parts), is_stat=self._is_stat(parts), stat=self._stat(parts), - siglevel=self._siglevel(parts)) + siglevel=self._siglevel(parts), + is_viewlike=viewlike) def _row_pattern(self, target_ci): """ From 2ecafe760ead625e4c3b46567708504f55e61909 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 17 Dec 2018 16:24:21 +0100 Subject: [PATCH 613/733] leftover code removval --- quantipy/sandbox/sandbox.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 93ec5e092..660160795 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2286,13 +2286,6 @@ def _views_per_rows(self): counts = counts + ['__viewlike__'] colpcts = colpcts + ['__viewlike__'] rowpcts = rowpcts + ['__viewlike__'] - # else: - # if ci == 'counts_colpct' and self.grouping: - # if not self._is_counts(parts): - # # ...or self._is_c_base(parts): - # colpcts.append(None) - # else: - # colpcts.extend([v] * self.views[v]) dims = self._frame.shape for row in range(0, dims[0]): if ci == 'counts_colpct' and self.grouping: From 8d7df30b6ce337c2116dada6afd6038c52bb2227 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 18 Dec 2018 15:56:53 +0100 Subject: [PATCH 614/733] fixing the wrong dict expression and the testing for an actual condition --- quantipy/core/batch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 79b0ea1b2..74264774d 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -549,7 +549,7 @@ def hide_empty(self, xks=True, summaries=True): ------- None """ - cond = {0: self.filter} if self.filter else None + cond = {self.filter: 0} if self.filter else None removed_sum = [] for x in self.xks[:]: if self.is_array(x): @@ -567,7 +567,11 @@ def hide_empty(self, xks=True, summaries=True): if sources[i-1] in self.xks: self.xks.remove(sources[i-1]) elif not self._is_array_item(x): - if self[self.take(cond), x].count() == 0: + if cond: + s = self[self.take(cond), x] + else: + s = self[x] + if s.count() == 0: self.xks.remove(x) if removed_sum: msg = "Dropping summaries for {} - all items hidden!" From 85eb351d5b27b3563a94a52da12e0621d60cb8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 20 Dec 2018 10:14:51 +0100 Subject: [PATCH 615/733] skip rules for checks --- quantipy/core/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 45c3f2761..c00866ea3 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2094,7 +2094,7 @@ def _add_checking_chain(self, dk, cluster, name, x, y, views): c_views = view_keys + [v for v in c_views if v.endswith('{}_check'.format(key))] if isinstance(cluster, ChainManager): - cluster.get('checks', 'no_filter', x, y, c_views, folder=name) + cluster.get('checks', 'no_filter', x, y, c_views, folder=name, rules=False) else: if name == 'stat_check': chain = c_stack.get_chain(x=x, y=y, views=c_views, orient_on='x') From 14b99ee98cef4a1135dc63d50215ac6fa518eca8 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 20 Dec 2018 11:12:26 +0100 Subject: [PATCH 616/733] rebase parameter for net views --- quantipy/core/quantify/engine.py | 83 +++++++++++----------- quantipy/core/stack.py | 8 ++- quantipy/core/view_generators/view_maps.py | 2 +- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 86756ff97..3845c2f71 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -75,6 +75,7 @@ def __init__(self, link, weight=None, base_all=False, ignore_flags=False): self.result = None self.logical_conditions = [] self.cbase = self.rbase = None + self.rebased = {} self.comb_x = self.comb_y = None self.calc_x = self.calc_y = None self._has_x_margin = self._has_y_margin = False @@ -221,7 +222,7 @@ def swap(self, var, axis='x', update_axis_def=True, inplace=True): org_sources = self.ds.sources(org_parent) else: org_sources = self.ds.sources(self.x) - if not len(org_sources) == len(new_sources): + if not len(org_sources) == len(new_sources) and array_swap: err = "Cannot swap array-type Quantity with array of different " err += "source items length ({} vs. {})!" err = err.format(len(org_sources), len(new_sources)) @@ -1536,17 +1537,43 @@ def _make_nest_multiindex(self): mi = pd.MultiIndex.from_product(values, names=names) return mi - def normalize(self, on='y'): + def rebase(self, on=None): + """ + Normalize to another's variable column margin (base). + + Parameters + ---------- + on : str, default None + The variable from which the new margin should be generated from. + ``None`` will reset any rebased result`. + + Returns + ------- + None + """ + if not on: + self.rebased = {} + return self + swapped = self.swap(on, inplace=False) + self.rebased = {on: swapped.count().cbase} + return self + + def normalize(self, on='y', rebase_on=None): """ Convert a raw cell count result to its percentage representation. + + .. note:: Will prioritize the self.rebased margin row if one is found. + Parameters ---------- on : {'y', 'x'}, default 'y' Defines the base to normalize the result on. ``'y'`` will produce column percentages, ``'x'`` will produce row percentages. - + rebase_on : str, default None + Use another variable's column margin for the computation. Only + allowed if ``on`` ```'y'``. Returns ------- self @@ -1571,12 +1598,22 @@ def normalize(self, on='y'): if self.x == '@': on = 'y' if on == 'x' else 'x' if on == 'y': if self._has_y_margin or self.y == '@' or self.x == '@': - base = self.cbase + if not rebase_on: + base = self.cbase + else: + self.rebase(rebase_on) + base = self.rebased.values()[0] else: - if self._get_type() == 'array': + if not rebase_on: base = self.cbase else: - base = self.cbase[:, 1:] + self.rebase(rebase_on) + base = self.rebased.values()[0] + + if self._get_type() != 'array': + base = base[:, 1:] + + elif on == 'x': if self._has_x_margin: base = self.rbase @@ -1594,40 +1631,6 @@ def normalize(self, on='y'): self.result = self.result.T return self - def rebase(self, reference, on='counts', overwrite_margins=True): - """ - """ - val_err = 'No frequency aggregation to rebase.' - if self.result is None: - raise ValueError(val_err) - elif self.current_agg != 'freq': - raise ValueError(val_err) - is_df = self._force_to_nparray() - has_margin = self._attach_margins() - ref = self.swap(var=reference, inplace=False) - if self._sects_identical(self.xdef, ref.xdef): - pass - elif self._sects_different_order(self.xdef, ref.xdef): - ref.xdef = self.xdef - ref._x_indexers = ref._get_x_indexers() - ref.matrix = ref.matrix[:, ref._x_indexers + [0]] - elif self._sect_is_subset(self.xdef, ref.xdef): - ref.xdef = [code for code in ref.xdef if code in self.xdef] - ref._x_indexers = ref._sort_indexer_as_codes(ref._x_indexers, - self.xdef) - ref.matrix = ref.matrix[:, [0] + ref._x_indexers] - else: - idx_err = 'Axis defintion is not a subset of rebase reference.' - raise IndexError(idx_err) - ref_freq = ref.count(as_df=False) - self.result = (self.result/ref_freq.result) * 100 - if overwrite_margins: - self.rbase = ref_freq.rbase - self.cbase = ref_freq.cbase - self._organize_margins(has_margin) - if is_df: self.to_df() - return self - @staticmethod def _sects_identical(axdef1, axdef2): return axdef1 == axdef2 diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 45c3f2761..c3c211ec9 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2273,7 +2273,7 @@ def _dissect_defs(ds, var, net_map, recode, text_prefix): @modify(to_list=['on_vars', '_batches']) - def add_nets(self, on_vars, net_map, expand=None, calc=None, text_prefix='Net:', + def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_prefix='Net:', checking_cluster=None, _batches='all', recode='auto', verbose=True): """ Add a net-like view to a specified collection of x keys of the stack. @@ -2306,6 +2306,9 @@ def add_nets(self, on_vars, net_map, expand=None, calc=None, text_prefix='Net:', >>> {'calc': ('net_1', add, 'net_2'), 'text': {'en-GB': 'UK CALC LAB', ... 'da-DK': 'DA CALC LAB', ... 'de-DE': 'DE CALC LAB'}} + rebase : str, default None + Use another variables margin's value vector for column percentage + computation. text_prefix : str, default 'Net:' By default each code grouping/net will have its ``text`` label prefixed with 'Net: '. Toggle by passing None (or an empty str, ''). @@ -2404,7 +2407,8 @@ def _check_and_update_calc(calc_expression, text_key): 'expand': expand if expand in ['after', 'before'] else None, 'complete': True if expand else False, 'calc': calc, - 'calc_only': calc_only} + 'calc_only': calc_only, + 'rebase': rebase} view.add_method('net', kwargs=options) self.aggregate(view, False, [], _batches, on_vars, verbose=verbose) diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index e9bb9c185..85dc10d98 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -328,7 +328,7 @@ def frequency(self, link, name, kwargs): if rel_to is not None: if q.type == 'array': rel_to = 'y' - q.normalize(rel_to) + q.normalize(rel_to, kwargs.get('rebase', None)) q.to_df() view.cbases = q.cbase view.rbases = q.rbase From 22b1b7067d416ffedf344d4f9e86504ca33ca135 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 20 Dec 2018 18:04:25 +0100 Subject: [PATCH 617/733] parameter name change from rebase to other_base. probably only temp. until optional rebasing to full cell swap works. --- quantipy/core/quantify/engine.py | 14 +++++++------- quantipy/core/stack.py | 2 +- quantipy/core/view_generators/view_maps.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 3845c2f71..3493723f4 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1539,7 +1539,7 @@ def _make_nest_multiindex(self): def rebase(self, on=None): """ - Normalize to another's variable column margin (base). + Instruct normalizing to another's variable column margin (base). Parameters ---------- @@ -1558,7 +1558,7 @@ def rebase(self, on=None): self.rebased = {on: swapped.count().cbase} return self - def normalize(self, on='y', rebase_on=None): + def normalize(self, on='y', other_base=None, on_elements=None): """ Convert a raw cell count result to its percentage representation. @@ -1571,7 +1571,7 @@ def normalize(self, on='y', rebase_on=None): Defines the base to normalize the result on. ``'y'`` will produce column percentages, ``'x'`` will produce row percentages. - rebase_on : str, default None + other_base : str, default None Use another variable's column margin for the computation. Only allowed if ``on`` ```'y'``. Returns @@ -1598,16 +1598,16 @@ def normalize(self, on='y', rebase_on=None): if self.x == '@': on = 'y' if on == 'x' else 'x' if on == 'y': if self._has_y_margin or self.y == '@' or self.x == '@': - if not rebase_on: + if not other_base: base = self.cbase else: - self.rebase(rebase_on) + self.rebase(other_base) base = self.rebased.values()[0] else: - if not rebase_on: + if not other_base: base = self.cbase else: - self.rebase(rebase_on) + self.rebase(other_base) base = self.rebased.values()[0] if self._get_type() != 'array': diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index c3c211ec9..2582fb8ca 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2408,7 +2408,7 @@ def _check_and_update_calc(calc_expression, text_key): 'complete': True if expand else False, 'calc': calc, 'calc_only': calc_only, - 'rebase': rebase} + 'other_base': rebase} view.add_method('net', kwargs=options) self.aggregate(view, False, [], _batches, on_vars, verbose=verbose) diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index 85dc10d98..34e0d2df4 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -328,7 +328,7 @@ def frequency(self, link, name, kwargs): if rel_to is not None: if q.type == 'array': rel_to = 'y' - q.normalize(rel_to, kwargs.get('rebase', None)) + q.normalize(rel_to, kwargs.get('other_base', None)) q.to_df() view.cbases = q.cbase view.rbases = q.rbase From 270ac643be96fbf3c38604d3199d8716b5605847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 21 Dec 2018 09:29:01 +0100 Subject: [PATCH 618/733] fix in derotate (for legacy data) --- quantipy/core/dataset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9ef686373..e88df6510 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6677,9 +6677,9 @@ def _derotate_meta(self, mapper, other): """ meta = self._meta new_meta = self.start_meta(self.text_key) - new_meta['info']['dataset'] = {'name': ''} - dname = '{}_derotate'.format(meta['info']['dataset']['name']) - new_meta['info']['dataset']['name'] = dname + n = meta['info'].get('dataset', meta['info']).get('name') + dname = '{}_derotate'.format(n) + new_meta['info']['dataset'] = {'name': dname} for var in other: new_meta = self._assume_meta(new_meta, var, var) From ca2eb31b8b630468c411bef9cef5311a07fbd71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 28 Dec 2018 10:31:38 +0100 Subject: [PATCH 619/733] fix in dataset empty_items for delimited set arrays --- quantipy/core/dataset.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e88df6510..69caace73 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6338,9 +6338,8 @@ def empty_items(self, name, condition=None, by_name=True): else: df = self._data.copy() for n in name: - test_df = df[self.unroll(n)].sum() - slicer = test_df == 0 - empty_items = test_df.loc[slicer].index.values.tolist() + empty_items = [i for i in self.unroll(n) + if df[i].value_counts().sum() == 0] if not by_name: empty_items = [self.item_no(i) for i in empty_items] if empty_items: empty[n] = empty_items if empty: From af358f745c61d138571f6aea61206e95ceee0c1a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 7 Jan 2019 12:38:43 +0100 Subject: [PATCH 620/733] checkpoint: new normalize method --- quantipy/core/quantify/engine.py | 56 +++++++++++++++------- quantipy/core/view_generators/view_maps.py | 18 ++++++- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 3493723f4..87bb7debe 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1558,30 +1558,55 @@ def rebase(self, on=None): self.rebased = {on: swapped.count().cbase} return self - def normalize(self, on='y', other_base=None, on_elements=None): + def _normalize_on_cells(self, other): + """ + """ + is_df = self._force_to_nparray() + other_q = self.swap(other, update_axis_def=False, inplace=False) + other_len = len(other_q.xdef) + q_len = len(self.xdef) + if not other_len == q_len: + err = "Cannot normalize on '{}', shapes do not match! ({} vs. {})" + raise ValueError(err.format(other, q_len, other_len)) + has_margin = self._attach_margins() + counts = other_q.count(as_df=False, margin=has_margin).result + self._organize_margins(has_margin) + self.result = (self.result / counts) * 100 + if is_df: self.to_df() + return None + + def normalize(self, on='y', per_cell=False): """ Convert a raw cell count result to its percentage representation. - .. note:: Will prioritize the self.rebased margin row if one is found. Parameters ---------- - on : {'y', 'x'}, default 'y' + on : {'y', 'x', 'counts_sum', str}, default 'y' Defines the base to normalize the result on. ``'y'`` will - produce column percentages, ``'x'`` will produce row - percentages. - other_base : str, default None - Use another variable's column margin for the computation. Only - allowed if ``on`` ```'y'``. + produce column percentages, ``'x'`` will produce row percentages. + It is also possible to use another question's frequencies to + compute rebased percentages providing its name instead. + per_cell : bool, default False + Compute percentages on a cell-per-cell basis, effectively treating + each category as a base figure on its own. Only possible if the + ``on`` argument does not indidcate an axis result (``'x'``, ``'y'``, + ``'counts_sum'``) but another variable name. The related ``xdef`` + codes length must be identical for this, otherwise a ``ValueError`` + is raised. + Returns ------- self - Updates an count-based aggregation in the ``result`` property. - """ - if on not in ['x', 'y', 'counts_sum']: - raise ValueError("'on' must be one of 'x', 'y' or 'counts_sum'.") - elif on == 'counts_sum' and (self.comb_x or self.comb_y): + Updates a count-based aggregation in the ``result`` property. + """ + # if on not in ['x', 'y', 'counts_sum']: + # raise ValueError("'on' must be one of 'x', 'y' or 'counts_sum'.") + rebase = on not in ['x', 'y', 'counts_sum'] + other_counts = rebase and per_cell + other_base = not other_counts + if on == 'counts_sum' and (self.comb_x or self.comb_y): raise ValueError("Groups cannot be normalized on 'counts_sum'") if on == 'counts_sum': is_df = self._force_to_nparray() @@ -1594,6 +1619,8 @@ def normalize(self, on='y', other_base=None, on_elements=None): self.result = self.result / base * 100 self._organize_margins(has_margin) if is_df: self.to_df() + elif other_counts: + self._normalize_on_cells(other_counts) else: if self.x == '@': on = 'y' if on == 'x' else 'x' if on == 'y': @@ -1609,11 +1636,8 @@ def normalize(self, on='y', other_base=None, on_elements=None): else: self.rebase(other_base) base = self.rebased.values()[0] - if self._get_type() != 'array': base = base[:, 1:] - - elif on == 'x': if self._has_x_margin: base = self.rbase diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index 34e0d2df4..7b0ea3b7d 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -288,7 +288,7 @@ def frequency(self, link, name, kwargs): """ view = View(link, name, kwargs=kwargs) axis, condition, rel_to, weights, text = view.get_std_params() - logic, expand, complete, calc, exclude, rescale = view.get_edit_params() + logic, expand, complete, calc, exclude, rescale = view.get_edit_params() # ==================================================================== # this block of kwargs should be removed # parameter overwriting should be done using the template @@ -328,7 +328,21 @@ def frequency(self, link, name, kwargs): if rel_to is not None: if q.type == 'array': rel_to = 'y' - q.normalize(rel_to, kwargs.get('other_base', None)) + if not rel_to in ['x', 'y']: + + rel_to_kind = rel_to.split('.') + if len(rel_to_kind) == 2: + if rel_to_kind[1] == 'counts': + per_cells = True + elif rel_to_kind == 'base': + per_cells = False + else: + per_cells = False + per_cells = = rel_to.split('.')[1] + rel_to = 'y' + + q.normalize(rel_to, + per_cells=rel_to.) q.to_df() view.cbases = q.cbase view.rbases = q.rbase From 9262f7852b1c753c953be54b659e242fa7be83c9 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 7 Jan 2019 12:56:19 +0100 Subject: [PATCH 621/733] another bugfix for grouping with multiple cell items, sig tests and array summaries --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 94045e226..7e95ee005 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2732,7 +2732,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, for tgv in test_given_views: for view in tgv: if view.split('|')[1].startswith('t.'): cond2 = True - if not(cond1 and cond2): + if not(cond1 and cond2) or (cond1 and self.array_style == 1): self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) # CONTINUED: # ------------------------------------------------------------ From 7736215036260e7abeb1035b663259ae0292c6e9 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 7 Jan 2019 15:41:43 +0100 Subject: [PATCH 622/733] normalize changes --- quantipy/core/quantify/engine.py | 20 ++++++------- quantipy/core/stack.py | 10 ++++--- quantipy/core/view_generators/view_maps.py | 34 ++++++++++++---------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 87bb7debe..0f5bb18ce 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1590,22 +1590,20 @@ def normalize(self, on='y', per_cell=False): compute rebased percentages providing its name instead. per_cell : bool, default False Compute percentages on a cell-per-cell basis, effectively treating - each category as a base figure on its own. Only possible if the + each categorical row as a base figure on its own. Only possible if the ``on`` argument does not indidcate an axis result (``'x'``, ``'y'``, - ``'counts_sum'``) but another variable name. The related ``xdef`` - codes length must be identical for this, otherwise a ``ValueError`` - is raised. + ``'counts_sum'``), but instead another variable's name. The related + ``xdef`` codes collection length must be identical for this for work, + otherwise a ``ValueError`` is raised. Returns ------- self Updates a count-based aggregation in the ``result`` property. """ - # if on not in ['x', 'y', 'counts_sum']: - # raise ValueError("'on' must be one of 'x', 'y' or 'counts_sum'.") rebase = on not in ['x', 'y', 'counts_sum'] other_counts = rebase and per_cell - other_base = not other_counts + other_base = rebase and not per_cell if on == 'counts_sum' and (self.comb_x or self.comb_y): raise ValueError("Groups cannot be normalized on 'counts_sum'") if on == 'counts_sum': @@ -1620,21 +1618,21 @@ def normalize(self, on='y', per_cell=False): self._organize_margins(has_margin) if is_df: self.to_df() elif other_counts: - self._normalize_on_cells(other_counts) + self._normalize_on_cells(on) else: if self.x == '@': on = 'y' if on == 'x' else 'x' - if on == 'y': + if on == 'y' or other_base: if self._has_y_margin or self.y == '@' or self.x == '@': if not other_base: base = self.cbase else: - self.rebase(other_base) + self.rebase(on) base = self.rebased.values()[0] else: if not other_base: base = self.cbase else: - self.rebase(other_base) + self.rebase(on) base = self.rebased.values()[0] if self._get_type() != 'array': base = base[:, 1:] diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 2582fb8ca..556b7666b 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2400,15 +2400,17 @@ def _check_and_update_calc(calc_expression, text_key): else: calc_only = False view = qp.ViewMapper() - view.make_template('frequency', {'rel_to': [None, 'y']}) - + if not rebase: + view.make_template('frequency', {'rel_to': [None, 'y']}) + else: + rebase = '{}.base'.format(rebase) + view.make_template('frequency', {'rel_to': [None, rebase]}) options = {'logic': netdef, 'axis': 'x', 'expand': expand if expand in ['after', 'before'] else None, 'complete': True if expand else False, 'calc': calc, - 'calc_only': calc_only, - 'other_base': rebase} + 'calc_only': calc_only} view.add_method('net', kwargs=options) self.aggregate(view, False, [], _batches, on_vars, verbose=verbose) diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index 7b0ea3b7d..f5cb7e32b 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -290,7 +290,7 @@ def frequency(self, link, name, kwargs): axis, condition, rel_to, weights, text = view.get_std_params() logic, expand, complete, calc, exclude, rescale = view.get_edit_params() # ==================================================================== - # this block of kwargs should be removed + # This block of kwargs should be removed # parameter overwriting should be done using the template # NOT QP core code! if kwargs.get('combine', False): @@ -302,6 +302,22 @@ def frequency(self, link, name, kwargs): logic_def['expand'] = None logic[no] = logic_def view._kwargs['logic'] = logic + # -------------------------------------------------------------------- + # This block of code resolves the rel_to arg. in order to be able to use + # rebased % computations. We are also adjusting for the regular notation + # string here... + # We need to avoid the forced overwriting of the kwarg and use the actual + # rel_to != 'x', 'y', 'counts_sum' string... + per_cell = False + if not rel_to in ['', None, 'x', 'y', 'counts_sum']: + view._kwargs['rel_to'] = 'y' + rel_to_kind = rel_to.split('.') + if len(rel_to_kind) == 2: + rel_to = rel_to_kind[0] + if rel_to_kind[1] == 'counts': + per_cell = True + elif rel_to_kind[1] == 'base': + per_cell = False # ==================================================================== w = weights if weights is not None else None ignore = True if name == 'cbase_gross' else False @@ -328,21 +344,7 @@ def frequency(self, link, name, kwargs): if rel_to is not None: if q.type == 'array': rel_to = 'y' - if not rel_to in ['x', 'y']: - - rel_to_kind = rel_to.split('.') - if len(rel_to_kind) == 2: - if rel_to_kind[1] == 'counts': - per_cells = True - elif rel_to_kind == 'base': - per_cells = False - else: - per_cells = False - per_cells = = rel_to.split('.')[1] - rel_to = 'y' - - q.normalize(rel_to, - per_cells=rel_to.) + q.normalize(rel_to, per_cell=per_cell) q.to_df() view.cbases = q.cbase view.rbases = q.rbase From 01b447b28c28ab5e06bd3e013aca934cc7307cad Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 08:15:13 +0100 Subject: [PATCH 623/733] removing rebase method --- quantipy/core/quantify/engine.py | 35 ++++++++------------------------ 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 0f5bb18ce..2890d911c 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1537,26 +1537,11 @@ def _make_nest_multiindex(self): mi = pd.MultiIndex.from_product(values, names=names) return mi - def rebase(self, on=None): + def _get_other_base(self, other): """ - Instruct normalizing to another's variable column margin (base). - - Parameters - ---------- - on : str, default None - The variable from which the new margin should be generated from. - ``None`` will reset any rebased result`. - - Returns - ------- - None """ - if not on: - self.rebased = {} - return self - swapped = self.swap(on, inplace=False) - self.rebased = {on: swapped.count().cbase} - return self + swapped = self.swap(other, inplace=False) + return swapped.count().cbase def _normalize_on_cells(self, other): """ @@ -1579,7 +1564,7 @@ def normalize(self, on='y', per_cell=False): """ Convert a raw cell count result to its percentage representation. - .. note:: Will prioritize the self.rebased margin row if one is found. + .. note:: Will prioritize the self.rebased margin row if one is found. Parameters ---------- @@ -1591,10 +1576,10 @@ def normalize(self, on='y', per_cell=False): per_cell : bool, default False Compute percentages on a cell-per-cell basis, effectively treating each categorical row as a base figure on its own. Only possible if the - ``on`` argument does not indidcate an axis result (``'x'``, ``'y'``, + ``on`` argument does not indidcate an axis result (``'x'``, ``'y'``, ``'counts_sum'``), but instead another variable's name. The related ``xdef`` codes collection length must be identical for this for work, - otherwise a ``ValueError`` is raised. + otherwise a ``ValueError`` is raised. Returns ------- @@ -1624,16 +1609,14 @@ def normalize(self, on='y', per_cell=False): if on == 'y' or other_base: if self._has_y_margin or self.y == '@' or self.x == '@': if not other_base: - base = self.cbase + base = self.cbase else: - self.rebase(on) - base = self.rebased.values()[0] + base = self._get_other_base(on) else: if not other_base: base = self.cbase else: - self.rebase(on) - base = self.rebased.values()[0] + base = self._get_other_base(on) if self._get_type() != 'array': base = base[:, 1:] elif on == 'x': From e43be741b219575a3e92d7c99b6319099c433860 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 09:08:47 +0100 Subject: [PATCH 624/733] dataset._typed_batches() to query batch names by type --- quantipy/core/dataset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9ef686373..2a00d1db9 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -211,6 +211,19 @@ def batches(self): else: return [] + def _typed_batches(self, kind): + """ + """ + verbose = self._verbose_infos + self._verbose_infos = False + batches = self.batches() + if kind == 'main': + typed_batches = [b for b in batches if not self.get_batch(b).additional] + elif kind == 'add': + typed_batches = [b for b in batches if self.get_batch(b).additional] + self._verbose_infos = verbose + return typed_batches + def set_verbose_errmsg(self, verbose=True): """ """ From bf7905bb51133d5120495a8cbabb445b015781ac Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 09:30:36 +0100 Subject: [PATCH 625/733] _adds_per_mains() method to show the ampping of adds to main/parent batches, also in reverse --- quantipy/core/dataset.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 2a00d1db9..a92b73255 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -224,6 +224,26 @@ def _typed_batches(self, kind): self._verbose_infos = verbose return typed_batches + def _adds_per_mains(self, reverse=False): + """ + """ + bmeta = self._meta['sets']['batches'] + adds_per_mains = {bname: bdef['additions'] for bname, bdef + in bmeta.items() if bdef['additions']} + if not reverse: + return adds_per_mains + else: + rev = {} + adds = self._typed_batches('add') + for add in adds: + for m, a in adds_per_mains.items(): + if add in a: + if add in rev: + rev[add].append(m) + else: + rev[add] = [m] + return rev + def set_verbose_errmsg(self, verbose=True): """ """ From a0508e4abe97ddfc5eab2141587e0df0a9c18912 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 09:58:46 +0100 Subject: [PATCH 626/733] dataset.batches() now able to separate between main and add batches --- quantipy/core/batch.py | 25 +++++++++++++++++++++++++ quantipy/core/dataset.py | 22 +++++++++++++--------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 79b0ea1b2..de47e3613 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -422,6 +422,31 @@ def as_addition(self, batch_name): return None + def as_main(self, keep=False): + """ + Transform additional ``Batch`` definitions into regular (parent/main) ones. + + Parameters + ---------- + keep : bool, default False + ``True`` will keep the original related parent Batch, while the + default is to drop it. + + Returns + ------- + None + """ + if not self.additional: return None + self.additional = False + bmeta = self._meta['sets']['batches'] + parent = self._adds_per_mains(True)[self.name] + for p in parent: + if not keep: + del bmeta[p] + else: + bmeta[p]['additions'].remove(self.name) + self._update() + @modify(to_list='varlist') def add_variables(self, varlist): """ diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index a92b73255..b4ce66536 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -205,22 +205,26 @@ def _net_view_recodes(self): def _by_property(self, prop): return [v for v in self.variables() if self.get_property(v, prop)] - def batches(self): - if 'batches' in self._meta['sets']: - return self._meta['sets']['batches'].keys() - else: + def batches(self, main=True, add=True): + if not 'batches' in self._meta['sets'] or (not main and not add): return [] + batches = self._meta['sets']['batches'].keys() + if main and add: + return batches + if main: + return self._typed_batches(batches, 'main') + if add: + return self._typed_batches(batches, 'add') - def _typed_batches(self, kind): + def _typed_batches(self, all_batches, kind): """ """ verbose = self._verbose_infos self._verbose_infos = False - batches = self.batches() if kind == 'main': - typed_batches = [b for b in batches if not self.get_batch(b).additional] + typed_batches = [b for b in all_batches if not self.get_batch(b).additional] elif kind == 'add': - typed_batches = [b for b in batches if self.get_batch(b).additional] + typed_batches = [b for b in all_batches if self.get_batch(b).additional] self._verbose_infos = verbose return typed_batches @@ -234,7 +238,7 @@ def _adds_per_mains(self, reverse=False): return adds_per_mains else: rev = {} - adds = self._typed_batches('add') + adds = self._typed_batches(bmeta.keys(), 'add') for add in adds: for m, a in adds_per_mains.items(): if add in a: From 4dadb7abe80df05695539fc4f394573adce65788 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 13:34:28 +0100 Subject: [PATCH 627/733] fixed tests for viewlike property --- tests/parameters_chain.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/parameters_chain.py b/tests/parameters_chain.py index 215eaaae9..dc900540f 100644 --- a/tests/parameters_chain.py +++ b/tests/parameters_chain.py @@ -301,7 +301,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 1: { 'is_block': False, 'is_c_base': False, @@ -334,7 +335,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 2: { 'is_block': False, 'is_c_base': False, @@ -367,7 +369,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 3: { 'is_block': False, 'is_c_base': False, @@ -400,7 +403,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 4: { 'is_block': False, 'is_c_base': False, @@ -433,7 +437,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 5: { 'is_block': False, 'is_c_base': False, @@ -466,7 +471,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 6: { 'is_block': False, 'is_c_base': False, @@ -499,7 +505,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 7: { 'is_block': False, 'is_c_base': False, @@ -532,7 +539,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': None, 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 8: { 'is_block': False, 'is_c_base': False, @@ -565,7 +573,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': 'mean', 'weight': None, - 'is_calc_only': False}, + 'is_calc_only': False, + 'is_viewlike': False}, 9: { 'is_block': False, 'is_c_base': False, @@ -598,7 +607,8 @@ def pad(level, size, pad_id): 'siglevel': None, 'stat': 'median', 'weight': None, - 'is_calc_only': False} + 'is_calc_only': False, + 'is_viewlike': False} } CHAIN_STRUCT_COLUMNS = ['record_number', 'age', 'gender', 'q9', 'q9a'] From ec6a46ab3b79143edfcad9f7459313a652470870 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 14:11:15 +0100 Subject: [PATCH 628/733] another bugfix for grouped views in array summaries... --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 7e95ee005..a687e7099 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2732,7 +2732,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, for tgv in test_given_views: for view in tgv: if view.split('|')[1].startswith('t.'): cond2 = True - if not(cond1 and cond2) or (cond1 and self.array_style == 1): + if not(cond1 and cond2) or cond1: self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) # CONTINUED: # ------------------------------------------------------------ From e3afd2ab8863c818d13d7a1c0f74ec240aa744b5 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 14:55:09 +0100 Subject: [PATCH 629/733] bugfix for too strict check if stats are applied on delimited sets --- quantipy/core/stack.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 45c3f2761..d8d86d49f 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2604,9 +2604,10 @@ def _add_factors(v, meta, values, args): meta = self[dk].meta data = self[dk].data check_on = [] + no_os = not other_source for v in on_vars: if v in meta['sets']: - if meta['masks'][v]['subtype'] == 'delimited set': + if meta['masks'][v]['subtype'] == 'delimited set' and no_os: w = warn + 'Stats are not valid on delimited sets!\n' print w.format(v) continue @@ -2617,7 +2618,7 @@ def _add_factors(v, meta, values, args): w = warn + 'No values found!\n' print w.format(v) continue - elif meta['columns'][v]['type'] == 'delimited set': + elif meta['columns'][v]['type'] == 'delimited set' and no_os: w = warn + 'Stats are not valid on delimited sets!\n' print w.format(v) continue From 6e65d797e81a65ef2c25fc5494265245d501e694 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 8 Jan 2019 16:19:27 +0100 Subject: [PATCH 630/733] bugfix: rebase on other base alone --- quantipy/core/quantify/engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 2890d911c..ef2847efa 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1627,7 +1627,7 @@ def normalize(self, on='y', per_cell=False): if isinstance(self.result, pd.DataFrame): if self.x == '@': self.result = self.result.T - if on == 'y': + if on == 'y' or other_base: base = np.repeat(base, self.result.shape[0], axis=0) else: base = np.repeat(base, self.result.shape[1], axis=1) From 30db3674dbc1ad8f1b94acaad9f50f72825c4727 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 10 Jan 2019 10:53:59 +0100 Subject: [PATCH 631/733] fixing inplace=False return dataframe --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9ef686373..69cd11c66 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2852,7 +2852,7 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', self.add_meta(org_wname, 'float', weight_description) self.update(data_wgt, on=unique_key) else: - return data_wgt + return engine.dataframe(weight_scheme.name) # ------------------------------------------------------------------------ # lists/ sets of variables/ data file items From a36285c048847bbd6d295eda42b0130aa64c2aa9 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 10 Jan 2019 11:33:34 +0100 Subject: [PATCH 632/733] updating variable name of weight for consistency between inplace=True and False --- quantipy/core/dataset.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 69cd11c66..80e1c2e2b 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2842,17 +2842,18 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', full_file_path = '{} ({}).xlsx'.format(path_report, weight_name) df.to_excel(full_file_path) print 'Weight report saved to:\n{}'.format(full_file_path) + s_name = weight_scheme.name + s_w_name = 'weights_{}'.format(s_name) if inplace: - scheme_name = weight_scheme.name - weight_name = 'weights_{}'.format(scheme_name) - weight_description = '{} weights'.format(scheme_name) - data_wgt = engine.dataframe(scheme_name)[[unique_key, weight_name]] - data_wgt.rename(columns={weight_name: org_wname}, inplace=True) + weight_description = '{} weights'.format(s_name) + data_wgt = engine.dataframe(s_name)[[unique_key, s_w_name]] + data_wgt.rename(columns={s_w_name: org_wname}, inplace=True) if org_wname not in self._meta['columns']: self.add_meta(org_wname, 'float', weight_description) self.update(data_wgt, on=unique_key) else: - return engine.dataframe(weight_scheme.name) + wdf = engine.dataframe(weight_scheme.name) + return wdf.rename(columns={s_w_name: org_wname}) # ------------------------------------------------------------------------ # lists/ sets of variables/ data file items From a6c0d704a4451528689fbc3444eb0612b91bdc29 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 11 Jan 2019 15:17:46 +0100 Subject: [PATCH 633/733] Adding missings() method to return all missings in the dataset --- quantipy/core/dataset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 80e1c2e2b..46975384e 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -205,6 +205,16 @@ def _net_view_recodes(self): def _by_property(self, prop): return [v for v in self.variables() if self.get_property(v, prop)] + @verify(variables={'name': 'both'}) + def missings(self, name=None): + if name: + return self._get_missing_map(name) + all_missings = {} + for v in self.variables(): + miss = self._get_missing_map(v) + if miss: all_missings[v] = miss + return all_missings + def batches(self): if 'batches' in self._meta['sets']: return self._meta['sets']['batches'].keys() From a22bd575008da0ff0c33bb32acc5f69f3085c060 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 14 Jan 2019 10:52:55 +0100 Subject: [PATCH 634/733] parameter deafult/name changes --- quantipy/core/batch.py | 8 ++++---- quantipy/core/view_generators/view_maps.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index c393ac269..2029239ff 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -422,15 +422,15 @@ def as_addition(self, batch_name): return None - def as_main(self, keep=False): + def as_main(self, keep=True): """ Transform additional ``Batch`` definitions into regular (parent/main) ones. Parameters ---------- - keep : bool, default False - ``True`` will keep the original related parent Batch, while the - default is to drop it. + keep : bool, default True + ``False`` will drop the original related parent Batch, while the + default is to keep it. Returns ------- diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index f5cb7e32b..4a9141a3e 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -288,7 +288,7 @@ def frequency(self, link, name, kwargs): """ view = View(link, name, kwargs=kwargs) axis, condition, rel_to, weights, text = view.get_std_params() - logic, expand, complete, calc, exclude, rescale = view.get_edit_params() + logic, expand, complete, calc, exclude, rescale = view.get_edit_params() # ==================================================================== # This block of kwargs should be removed # parameter overwriting should be done using the template @@ -314,9 +314,9 @@ def frequency(self, link, name, kwargs): rel_to_kind = rel_to.split('.') if len(rel_to_kind) == 2: rel_to = rel_to_kind[0] - if rel_to_kind[1] == 'counts': + if rel_to_kind[1] == 'cells': per_cell = True - elif rel_to_kind[1] == 'base': + elif rel_to_kind[1] == 'y': per_cell = False # ==================================================================== w = weights if weights is not None else None From 8879ebf0e047fb0200916a15643834c2252fb6a2 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 14 Jan 2019 12:13:34 +0100 Subject: [PATCH 635/733] bugfix (yet another one....) for array summaries and grouped views... --- quantipy/sandbox/sandbox.py | 38 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 660160795..069c87d4a 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1805,7 +1805,7 @@ def __init__(self, chain): return None def _insert_viewlikes(self, new_index_flat, org_index_mapped): - inserts = [new_index_flat.index(val) for val in new_index_flat + inserts = [new_index_flat.index(val) for val in new_index_flat if not val in org_index_mapped.values()] flatviews = [] for name, no in self.org_views.items(): @@ -1829,10 +1829,10 @@ def _updated_index_tuples(self, axis): else: current = self.df.index.values.tolist() mapped = self._idx_valmap - org_tuples = self._org_idx.tolist() + org_tuples = self._org_idx.tolist() merged = [mapped[val] if val in mapped else val for val in current] # ================================================================ - if (self.array_mi and axis == 1) or axis == 0: + if (self.array_mi and axis == 1) or axis == 0: self._transf_views = self._insert_viewlikes(merged, mapped) else: self._transf_views = self._org_views @@ -1856,7 +1856,7 @@ def _reindex(self): if not self.array_mi: x_names = y_names else: - x_names = ['Array', 'Questions'] + x_names = ['Array', 'Questions'] tuples = self._updated_index_tuples(axis=0) self.df.index = pd.MultiIndex.from_tuples(tuples, names=x_names) tuples = self._updated_index_tuples(axis=1) @@ -2711,7 +2711,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, self._has_rules = ['x', 'y'] else: self._has_rules = rules - + # use_views = views[:] # for first in self.axes[0]: # for second in self.axes[1]: @@ -2720,7 +2720,7 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, # for v in use_views: # if v not in link: # use_views.remove(v) - + for first in self.axes[0]: found = [] x_frames = [] @@ -2764,14 +2764,18 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, # ------------------------------------------------------------ # if not any(len(v) == 2 and any(view.split('|')[1].startswith('t.') # for view in v) for v in self._given_views): - - test_given_views = [v if isinstance(v, (tuple, list)) else [v] for v in self._given_views] - cond1 = any(len(v) >= 2 for v in test_given_views) - cond2 = False - for tgv in test_given_views: - for view in tgv: - if view.split('|')[1].startswith('t.'): cond2 = True - if not(cond1 and cond2): + scan_views = [v if isinstance(v, (tuple, list)) else [v] + for v in self._given_views] + scan_views = [v for v in scan_views if len(v) > 1] + no_tests = [] + for scan_view in scan_views: + new_views = [] + for view in scan_view: + if not view.split('|')[1].startswith('t.'): + new_views.append(view) + no_tests.append(new_views) + cond = any(len(v) >= 2 for v in no_tests) + if cond: self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) # CONTINUED: # ------------------------------------------------------------ @@ -3125,7 +3129,7 @@ def _replace_test_results(df, replacement_map, char_repr): elif char_repr == 'lower': case = 'low' elif char_repr == 'alternate': - if case == 'up': + if case == 'up': case = 'low' else: case = 'up' @@ -3133,7 +3137,7 @@ def _replace_test_results(df, replacement_map, char_repr): v = [char.replace(str(no), l if case == 'up' else l.lower()) if isinstance(char, (str, unicode)) else char for char in v] - + new_values.append(v) else: new_values.append(v) @@ -3182,7 +3186,7 @@ def _siglevel_on_row(self): vpr = self._views_per_rows() tests = [(no, v) for no, v in enumerate(vpr) if v.split('|')[1].startswith('t.')] - s = [(t[0], + s = [(t[0], float(int(t[1].split('|')[1].split('.')[3].split('+')[0]))/100.0) for t in tests] return s From 49ea10ff2059dfafe3456adb13afea10f61ffdb5 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 14 Jan 2019 12:14:27 +0100 Subject: [PATCH 636/733] removing old code --- quantipy/sandbox/sandbox.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 069c87d4a..81dd093ea 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2760,10 +2760,6 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, if self._group_style == 'reduced' and self.array_style >- 1: - # OLD CHECK: - # ------------------------------------------------------------ - # if not any(len(v) == 2 and any(view.split('|')[1].startswith('t.') - # for view in v) for v in self._given_views): scan_views = [v if isinstance(v, (tuple, list)) else [v] for v in self._given_views] scan_views = [v for v in scan_views if len(v) > 1] @@ -2777,11 +2773,6 @@ def get(self, data_key, filter_key, x_keys, y_keys, views, rules=False, cond = any(len(v) >= 2 for v in no_tests) if cond: self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) - # CONTINUED: - # ------------------------------------------------------------ - # elif any(len(v) == 3 for v in self._given_views): - # self._frame = self._reduce_grouped_index(self._frame, 2, self._array_style) - if self.axis == 1: self.views = found[-1] else: From 5ac2ff9c158169ec935782ee84e63b388ff00823 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 14 Jan 2019 18:37:33 +0100 Subject: [PATCH 637/733] skiping sigtests for rebased freqs --- quantipy/core/quantify/engine.py | 7 ++++--- quantipy/core/view_generators/view_maps.py | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index ef2847efa..d81a00243 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -75,7 +75,6 @@ def __init__(self, link, weight=None, base_all=False, ignore_flags=False): self.result = None self.logical_conditions = [] self.cbase = self.rbase = None - self.rebased = {} self.comb_x = self.comb_y = None self.calc_x = self.calc_y = None self._has_x_margin = self._has_y_margin = False @@ -1564,8 +1563,6 @@ def normalize(self, on='y', per_cell=False): """ Convert a raw cell count result to its percentage representation. - .. note:: Will prioritize the self.rebased margin row if one is found. - Parameters ---------- on : {'y', 'x', 'counts_sum', str}, default 'y' @@ -1686,6 +1683,7 @@ def __init__(self, link, view_name_notation, test_total=False): self.Quantity.swap(var=view.has_other_source()) cond = {orgx: not_count(0)} self.Quantity.filter(cond, keep_base=False, inplace=True) + self.rebased = view._kwargs.get('rebased', False) self._set_baseline_aggregates(view) # Set information about the incoming aggregation # to be able to route correctly through the algorithms @@ -1838,6 +1836,9 @@ def set_params(self, test_total=False, level='mid', mimic='Dim', testtype='poole self.no_diffs = True if self.y == '@': self.no_pairs = True + if self.rebased: + self.invalid = True + self.no_pairs = True if self.invalid: self.mimic = mimic self.comparevalue, self.level = self._convert_level(level) diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index 4a9141a3e..fabd4ebd8 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -318,6 +318,10 @@ def frequency(self, link, name, kwargs): per_cell = True elif rel_to_kind[1] == 'y': per_cell = False + try: + link['x|f|:||{}|counts'.format(weights)]._kwargs['rebased'] = True + except: + pass # ==================================================================== w = weights if weights is not None else None ignore = True if name == 'cbase_gross' else False From 000431dde99ef2cafdf772769e67153b8cbcade3 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 15 Jan 2019 13:42:50 +0100 Subject: [PATCH 638/733] bugfix for export/assign --- quantipy/sandbox/sandbox.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 81dd093ea..7ca5a7f86 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1835,7 +1835,7 @@ def _updated_index_tuples(self, axis): if (self.array_mi and axis == 1) or axis == 0: self._transf_views = self._insert_viewlikes(merged, mapped) else: - self._transf_views = self._org_views + self._transf_views = self.org_views # ================================================================ i = d = 0 new_tuples = [] @@ -1857,10 +1857,10 @@ def _reindex(self): x_names = y_names else: x_names = ['Array', 'Questions'] - tuples = self._updated_index_tuples(axis=0) - self.df.index = pd.MultiIndex.from_tuples(tuples, names=x_names) tuples = self._updated_index_tuples(axis=1) self.df.columns = pd.MultiIndex.from_tuples(tuples, names=y_names) + tuples = self._updated_index_tuples(axis=0) + self.df.index = pd.MultiIndex.from_tuples(tuples, names=x_names) return None def export(self): @@ -2307,7 +2307,8 @@ def _views_per_rows(self): def _valid_views(self, flat=False): clean_view_list = [] valid = self.views.keys() - for v in self._given_views: + viewlikes = [v for v in valid if v.startswith('__viewlike__')] + for v in self._given_views + viewlikes: if isinstance(v, (str, unicode)): if v in valid: clean_view_list.append(v) From e8f80dfb42294fb0db35bdffe40f30c47074d6ed Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 15 Jan 2019 14:13:24 +0100 Subject: [PATCH 639/733] bugfix for tests --- quantipy/sandbox/sandbox.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 7ca5a7f86..3aafcc90c 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2307,8 +2307,12 @@ def _views_per_rows(self): def _valid_views(self, flat=False): clean_view_list = [] valid = self.views.keys() - viewlikes = [v for v in valid if v.startswith('__viewlike__')] - for v in self._given_views + viewlikes: + org_vc = self._given_views + v_likes = [v for v in valid if v.startswith('__viewlike__')] + if isinstance(org_vc, tuple): + v_likes = tuple(v_likes) + view_coll = org_vc + v_likes + for v in view_coll: if isinstance(v, (str, unicode)): if v in valid: clean_view_list.append(v) From b6feaa407bcb95c69ecbf00b6ecd2a0ae96c07b1 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 16 Jan 2019 16:14:29 +0100 Subject: [PATCH 640/733] skip set long if value exceed Dims limit --- .../core/tools/dp/dimensions/_create_ddf.dms | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/quantipy/core/tools/dp/dimensions/_create_ddf.dms b/quantipy/core/tools/dp/dimensions/_create_ddf.dms index a7e75224b..5c8346e88 100644 --- a/quantipy/core/tools/dp/dimensions/_create_ddf.dms +++ b/quantipy/core/tools/dp/dimensions/_create_ddf.dms @@ -59,11 +59,17 @@ Event(OnJobStart) Set oExcel = createobject("Excel.Application") if oExcel.DecimalSeparator="," Then dmgrJob.GlobalVariables.DecimalSeparator = "," + dmgrGlobal.Add("vars_failed") + dmgrGlobal.Add("var_failed") + End Event Event(OnNextCase) + const MaxValue = 2147483647 + const MinValue = -2147483648 + Dim i, oA, oA_value Dim columns, col, data Dim gSplit, gName, gElement, gField @@ -86,7 +92,25 @@ Event(OnNextCase) Set oA = dmgrJob.Questions[col] End If Select Case oA.QuestionDataType - Case 1, 6, 7 + Case 1 + If data[i] = "NULL" Then + oA = NULL + Else + oA_value=data[i] + if dmgrJob.GlobalVariables.DecimalSeparator = "," Then + oA_value = Replace(CText(oA_value),".",",") + End If + If CDouble(oA_value) > MaxValue or CDouble(oA_value) < MinValue then + Debug.Log("Value: " + CText(oA_value) + " for variable: '" + oA.label + "' exceeds limit for variable of type Long. Dropping set variable") + If dmgrGlobal.var_failed <> oA.label Then + dmgrGlobal.vars_failed = dmgrGlobal.vars_failed + oA.label + ", " + dmgrGlobal.var_failed = oA.label + End If + Else + oA = oA_value + End If + End If + Case 6, 7 If data[i] = "NULL" Then oA = NULL Else @@ -123,3 +147,16 @@ Event(OnNextCase) Next End Event + +Event (OnJobEnd) + + Debug.Log("**************************************************************************************************") + Debug.Log("Please note: ") + Debug.Log("For one or more cases a value exceeded the limit for Dimensions variable type 'Long'.") + Debug.Log("For those cases setting the variable were skipped.") + Debug.Log("The variables affected are: " + dmgrGlobal.vars_failed) + Debug.Log("If you need those variables then change DataType from 'mr.Long' to 'mr.Double' by editing 'create_mdd [].mrs' for affected variables") + Debug.Log("") + +End Event + From d796b1782196e818df83a5d7708eea17a00bb207 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 16 Jan 2019 16:25:54 +0100 Subject: [PATCH 641/733] add condition to OnJobEnd --- .../core/tools/dp/dimensions/_create_ddf.dms | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/quantipy/core/tools/dp/dimensions/_create_ddf.dms b/quantipy/core/tools/dp/dimensions/_create_ddf.dms index 5c8346e88..a9c0bc901 100644 --- a/quantipy/core/tools/dp/dimensions/_create_ddf.dms +++ b/quantipy/core/tools/dp/dimensions/_create_ddf.dms @@ -60,7 +60,8 @@ Event(OnJobStart) if oExcel.DecimalSeparator="," Then dmgrJob.GlobalVariables.DecimalSeparator = "," dmgrGlobal.Add("vars_failed") - dmgrGlobal.Add("var_failed") + dmgrGlobal.Add("var_failed") + dmgrGlobal.vars_failed = "" End Event @@ -149,14 +150,16 @@ Event(OnNextCase) End Event Event (OnJobEnd) - - Debug.Log("**************************************************************************************************") - Debug.Log("Please note: ") - Debug.Log("For one or more cases a value exceeded the limit for Dimensions variable type 'Long'.") - Debug.Log("For those cases setting the variable were skipped.") - Debug.Log("The variables affected are: " + dmgrGlobal.vars_failed) - Debug.Log("If you need those variables then change DataType from 'mr.Long' to 'mr.Double' by editing 'create_mdd [].mrs' for affected variables") - Debug.Log("") + + If dmgrGlobal.vars_failed <> "" Then + Debug.Log("**************************************************************************************************") + Debug.Log("Please note: ") + Debug.Log("For one or more cases a value exceeded the limit for Dimensions variable type 'Long'.") + Debug.Log("For those cases setting the variable were skipped.") + Debug.Log("The variables affected are: " + dmgrGlobal.vars_failed) + Debug.Log("If you need those variables then change DataType from 'mr.Long' to 'mr.Double' by editing 'create_mdd [].mrs' for affected variables") + Debug.Log("") + End If End Event From 61da8fc52efe23ed1c6e3d06286e34ffd1560ca0 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 25 Jan 2019 14:48:09 +0100 Subject: [PATCH 642/733] add_section method and sections property for Batch class --- quantipy/core/batch.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 2029239ff..d2826b6d9 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -129,6 +129,7 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.xks = [] self.yks = ['@'] self._variables = [] + self._section_starts = {} self.total = True self.extended_yks_global = None self.extended_yks_per_x = {} @@ -188,7 +189,8 @@ def _update(self): 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', - 'unwgt_counts', 'y_filter_map', 'build_info' + 'unwgt_counts', 'y_filter_map', 'build_info', + '_section_starts' ]: attr_update = {attr: attrs.get(attr, attrs.get('_{}'.format(attr)))} self._meta['sets']['batches'][self.name].update(attr_update) @@ -205,7 +207,7 @@ def _load_batch(self): 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', - 'y_filter_map', 'build_info', + 'y_filter_map', 'build_info', '_section_starts' ]: attr_load = {attr: bdefs.get(attr, bdefs.get('_{}'.format(attr)))} self.__dict__.update(attr_load) @@ -556,6 +558,37 @@ def extend_x(self, ext_xks): self._update() return None + def add_section(self, x_anchor, section): + """ + """ + if not isinstance(section, (unicode, str)): + raise TypeError("'section' must be a string.") + if x_anchor in self.xks: + self._section_starts[x_anchor] = section + self._update() + return None + + @property + def sections(self): + """ + """ + sects = self._section_starts + full_sections = OrderedDict() + rev_full_sections = OrderedDict() + for x in self.xks: + if x in sects: + full_sections[x] = sects[x] + last_group = sects[x] + else: + full_sections[x] = last_group + for k, v in full_sections.items(): + if v in rev_full_sections: + rev_full_sections[v].append(k) + else: + rev_full_sections[v] = [k] + return rev_full_sections + + def hide_empty(self, xks=True, summaries=True): """ Drop empty variables and hide array items from summaries. From 538a6af38484a089bd6c160af77b1bc2522754fd Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Fri, 25 Jan 2019 15:40:00 +0100 Subject: [PATCH 643/733] Batch.sections now returning None if no sections are defined --- quantipy/core/batch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index d2826b6d9..8e2724325 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -572,6 +572,7 @@ def add_section(self, x_anchor, section): def sections(self): """ """ + if not self._section_starts: return None sects = self._section_starts full_sections = OrderedDict() rev_full_sections = OrderedDict() From 0a882a9dbfaffb1b51dfb4198d9faf81689d7600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 28 Jan 2019 13:06:55 +0100 Subject: [PATCH 644/733] option to take missings into account for recoded nets --- quantipy/core/stack.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 797a9fbf5..1f725dc81 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2107,7 +2107,8 @@ def _add_checking_chain(self, dk, cluster, name, x, y, views): @staticmethod def recode_from_net_def(dataset, on_vars, net_map, expand, recode='auto', - text_prefix='Net:', verbose=True): + text_prefix='Net:', respect_missings_in_recodes=True, + verbose=True): """ Create variables from net definitions. """ @@ -2215,8 +2216,14 @@ def _dissect_defs(ds, var, net_map, recode, text_prefix): # order, remove codes if 'collect_codes' in recode: - other_logic = intersection( - [{var: not_count(0)}, {name: has_count(0)}]) + if respect_missings_in_recodes and dataset._get_missing_list(var): + other_logic = intersection([ + {var: not_count(0)}, + {name: has_count(0)}, + {var: not_any(dataset._get_missing_list(var))}]) + else: + other_logic = intersection( + [{var: not_count(0)}, {name: has_count(0)}]) if dataset._is_array_item(var) or dataset.take(other_logic).tolist(): cat_name = recode.split('@')[-1] if '@' in recode else 'Other' code = len(mapper)+1 @@ -2274,7 +2281,8 @@ def _dissect_defs(ds, var, net_map, recode, text_prefix): @modify(to_list=['on_vars', '_batches']) def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_prefix='Net:', - checking_cluster=None, _batches='all', recode='auto', verbose=True): + checking_cluster=None, _batches='all', recode='auto', + respect_missings_in_recodes=True, verbose=True): """ Add a net-like view to a specified collection of x keys of the stack. @@ -2308,7 +2316,7 @@ def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_p ... 'de-DE': 'DE CALC LAB'}} rebase : str, default None Use another variables margin's value vector for column percentage - computation. + computation. text_prefix : str, default 'Net:' By default each code grouping/net will have its ``text`` label prefixed with 'Net: '. Toggle by passing None (or an empty str, ''). @@ -2326,6 +2334,9 @@ def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_p the variable contains nets and another category that summarises all codes which are not included in any net. If no cat_name is provided, 'Other' is taken as default + respect_missings_in_recodes: bool, default True + Skip or include codes that are defined as missing when recoding + from net definition. Returns ------- None @@ -2419,7 +2430,8 @@ def _check_and_update_calc(calc_expression, text_key): ds.from_stack(self, dk) on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] self.recode_from_net_def(ds, on_vars, net_map, expand, recode, - text_prefix, verbose) + text_prefix, respect_missings_in_recodes, + verbose) if checking_cluster in [None, False] or only_recode: continue if isinstance(checking_cluster, ChainManager): From 02a50d631ce70e109ae821b9c73ed0c8f04aec5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 28 Jan 2019 13:18:00 +0100 Subject: [PATCH 645/733] rename parameter --- quantipy/core/stack.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 1f725dc81..d627df857 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2107,7 +2107,7 @@ def _add_checking_chain(self, dk, cluster, name, x, y, views): @staticmethod def recode_from_net_def(dataset, on_vars, net_map, expand, recode='auto', - text_prefix='Net:', respect_missings_in_recodes=True, + text_prefix='Net:', mis_in_rec=False, verbose=True): """ Create variables from net definitions. @@ -2216,7 +2216,7 @@ def _dissect_defs(ds, var, net_map, recode, text_prefix): # order, remove codes if 'collect_codes' in recode: - if respect_missings_in_recodes and dataset._get_missing_list(var): + if not mis_in_rec and dataset._get_missing_list(var): other_logic = intersection([ {var: not_count(0)}, {name: has_count(0)}, @@ -2280,9 +2280,9 @@ def _dissect_defs(ds, var, net_map, recode, text_prefix): @modify(to_list=['on_vars', '_batches']) - def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_prefix='Net:', - checking_cluster=None, _batches='all', recode='auto', - respect_missings_in_recodes=True, verbose=True): + def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, + text_prefix='Net:', checking_cluster=None, _batches='all', + recode='auto', mis_in_rec=False, verbose=True): """ Add a net-like view to a specified collection of x keys of the stack. @@ -2334,7 +2334,7 @@ def add_nets(self, on_vars, net_map, expand=None, calc=None, rebase=None, text_p the variable contains nets and another category that summarises all codes which are not included in any net. If no cat_name is provided, 'Other' is taken as default - respect_missings_in_recodes: bool, default True + mis_in_rec: bool, default False Skip or include codes that are defined as missing when recoding from net definition. Returns @@ -2430,7 +2430,7 @@ def _check_and_update_calc(calc_expression, text_key): ds.from_stack(self, dk) on_vars = [x for x in on_vars if x in self.describe('x').index.tolist()] self.recode_from_net_def(ds, on_vars, net_map, expand, recode, - text_prefix, respect_missings_in_recodes, + text_prefix, mis_in_rec, verbose) if checking_cluster in [None, False] or only_recode: continue From 333a77e6e324895e995167fde48846f339d9fec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 30 Jan 2019 09:45:05 +0100 Subject: [PATCH 646/733] adjust batch tests --- tests/test_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_batch.py b/tests/test_batch.py index caec5d33c..0291bd8f1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -49,7 +49,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 31) + self.assertEqual(len(_get_meta(batch1).keys()), 32) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) From bbc98fd196c34f673e504ba95230dba60c330d8a Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 31 Jan 2019 10:58:06 +0100 Subject: [PATCH 647/733] counts_first check for cell_items string --- quantipy/sandbox/sandbox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 3aafcc90c..bfbfd2930 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2059,7 +2059,10 @@ def cell_items(self): if c_colrow_pct: return 'counts_colpct_rowpct' elif c_colpct: - return 'counts_colpct' + if self._counts_first: + return 'counts_colpct' + else: + return 'colpct_counts' else: return 'counts_rowpct' From 15f8a97b57b45ea64544abc3cc402a09f73594da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 31 Jan 2019 12:40:45 +0100 Subject: [PATCH 648/733] Batch.codes_in_data() --- quantipy/core/batch.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 2029239ff..3cbfd70c0 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -556,6 +556,25 @@ def extend_x(self, ext_xks): self._update() return None + @verify(variables={'name': 'columns'}, categorical='name') + def codes_in_data(self, name): + """ + Get a list of codes that exist in (batch filtered) data. + """ + slicer = self.manifest_filter(self.filter) + data = self._data.copy().ix[slicer, name] + if self.is_delimited_set(name): + if not data.dropna().empty: + data_codes = data.str.get_dummies(';').columns.tolist() + data_codes = [int(c) for c in data_codes] + else: + data_codes = [] + else: + data_codes = pd.get_dummies(data).columns.tolist() + return data_codes + + + def hide_empty(self, xks=True, summaries=True): """ Drop empty variables and hide array items from summaries. From a7ec35b34c03151b6c0d5a687458f87c047bb385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 31 Jan 2019 13:54:52 +0100 Subject: [PATCH 649/733] remove lazy property --- quantipy/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index bfbfd2930..70c29de62 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2195,7 +2195,7 @@ def _fill_cells(self): return None - @lazy_property + # @lazy_property def _counts_first(self): for v in self.views: sname = v.split('|')[-1] From 495fa444092da2efafef49b93dad77dc85d7df62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 6 Feb 2019 12:39:07 +0100 Subject: [PATCH 650/733] bugfix in test engine --- quantipy/core/quantify/engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index d81a00243..cfd07eb0a 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -1750,7 +1750,7 @@ def _set_baseline_aggregates(self, view): self.values = self.values[:, 1:] self.cbases = self.cbases[:, 1:] elif self.metric == 'proportions': - if not self.test_total: + if not self.test_total or self.rebased: if view.is_cumulative(): agg = self.Quantity.count( margin=False, as_df=False, cum_sum=False) From 26196897617e382c91386141baa11b07e73eb9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 7 Feb 2019 09:57:42 +0100 Subject: [PATCH 651/733] fix in make_dummy for delimited set arrays --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index baeea38a6..f382a9d4d 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6915,7 +6915,7 @@ def make_dummy(self, var, partitioned=False): codes.extend(self._data[i].dropna().unique().tolist()) codes = sorted(list(set(codes))) dummy_data = [] - if self._is_multicode_array(items[0]): + if any(self._is_multicode_array(i) for i in items): for i in items: try: i_dummy = self[i].str.get_dummies(';') From 1dcb167666885e75eaa4f8a5bc6399e8484eb5e6 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Thu, 7 Feb 2019 13:47:26 +0100 Subject: [PATCH 652/733] bugfix for cell_items() not being called but simply evaluated... --- quantipy/sandbox/sandbox.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 70c29de62..af218ddb1 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -2059,7 +2059,7 @@ def cell_items(self): if c_colrow_pct: return 'counts_colpct_rowpct' elif c_colpct: - if self._counts_first: + if self._counts_first(): return 'counts_colpct' else: return 'colpct_counts' @@ -2291,14 +2291,14 @@ def _views_per_rows(self): rowpcts = rowpcts + ['__viewlike__'] dims = self._frame.shape for row in range(0, dims[0]): - if ci == 'counts_colpct' and self.grouping: + if ci in ['counts_colpct', 'colpct_counts'] and self.grouping: if row % 2 == 0: - if self._counts_first: + if self._counts_first(): vc = counts else: vc = colpcts else: - if not self._counts_first: + if not self._counts_first(): vc = counts else: vc = colpcts From f89aeecd8ec113bff208e46ac9402f39eb54cd6d Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Thu, 18 Oct 2018 21:47:21 +0200 Subject: [PATCH 653/733] add sig-test params to chart dict --- quantipy/sandbox/pptx/pptx_defaults.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/pptx_defaults.py b/quantipy/sandbox/pptx/pptx_defaults.py index 9b3a00264..f96966b39 100644 --- a/quantipy/sandbox/pptx/pptx_defaults.py +++ b/quantipy/sandbox/pptx/pptx_defaults.py @@ -188,7 +188,10 @@ # Number format ('number_format', '0.00%'), - ('xl_number_format', '0.00%') + ('xl_number_format', '0.00%'), + # Sig_testing + ('sig_test_visible', False), + ('sig_test_results', None), ]) # -------------------------- From e659083116a11b556693bf1d1de27aedce792ab3 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Thu, 18 Oct 2018 21:51:16 +0200 Subject: [PATCH 654/733] add comma to last key-value set in dicts for git usage --- quantipy/sandbox/pptx/pptx_defaults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantipy/sandbox/pptx/pptx_defaults.py b/quantipy/sandbox/pptx/pptx_defaults.py index f96966b39..fa47586b3 100644 --- a/quantipy/sandbox/pptx/pptx_defaults.py +++ b/quantipy/sandbox/pptx/pptx_defaults.py @@ -18,7 +18,7 @@ ('font_underline', False), ('font_color', (89, 89, 89)), ('font_color_brightness', 0), - ('font_color_theme', None) + ('font_color_theme', None), ]) # Chart Legend @@ -47,7 +47,7 @@ ('margin_bottom', 46800), # Cm(0.13) ('vertical_alignment', 'top'), ('horizontal_alignment', 'left'), - ('font_kwargs', default_font.copy()) + ('font_kwargs', default_font.copy()), ]) # --------------------------------------------------------------------------- @@ -62,7 +62,7 @@ ('textbox_fill_solid', False), ('textbox_color', (100, 0, 0)), ('textbox_color_brightness', 0), - ('textframe_kwargs', default_textframe.copy()) + ('textframe_kwargs', default_textframe.copy()), ]) # --------------------------------------------------------------------------- From 56f4468c8bc9669ac1a18fece1e442230144076b Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Tue, 12 Feb 2019 17:58:52 +0100 Subject: [PATCH 655/733] get PptxPainterClass.py up-to-date with staging-develop --- quantipy/sandbox/pptx/PptxPainterClass.py | 258 +++++++++++++++------- 1 file changed, 173 insertions(+), 85 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index 14fbbedff..a8aa050e1 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -2,6 +2,7 @@ import re from lxml import etree +import warnings # Imports from Python-PPTX from pptx import Presentation @@ -30,11 +31,11 @@ ) from PptxDefaultsClass import PptxDefaults +from PptxChainClass import float2String import pandas as pd import copy -# chartdata_from_dataframe taken from topy.core.pandas_pptx.py def chartdata_from_dataframe(df, number_format="0%", xl_number_format='0.00%'): """ Return a CategoryChartData instance from the given Pandas DataFrame. @@ -97,7 +98,6 @@ def get_parent(sub_categories, line, pos): return cd -# return_slide_layout_by_name is taken from quantipy.core.builds.powerpoint.visual_editor.py def return_slide_layout_by_name(pptx, slide_layout_name): """ Loop over the slide layout object and find slide layout by name, return slide layout @@ -116,6 +116,18 @@ def return_slide_layout_by_name(pptx, slide_layout_name): sld_layout=slide_layout_name)) +def convertable(obj, func): + """ + Returns True if obj can be converted by func without an error. + """ + + try: + func(obj) + return True + except ValueError: + return False + + class PptxPainter(object): """ A convenience wrapper around the python-pptx library @@ -169,6 +181,121 @@ def __init__(self, path_to_presentation, slide_layout=None, shape_properties=Non 'side_tables': {}, } + @staticmethod + def get_plot_values(plot): + """ + Return a list of dicts with serie name as dict-key and serie values as dict-value + + Parameters + ---------- + plot: pptx.chart.plot._BasePlot + + Returns + ------- + list + + """ + series = [ + {series.name: [str(s) for s in series.values]} + for series in plot.series + ] + + return series + + def show_data_labels(self, plot, decimals=0): + """ + Explicitly sets datalabels to allow for datalabel editing. + + Parameters + ---------- + plot: pptx.chart.plot._BasePlot + The plot object for which datalabels need should be shown. + decimals: the number of decimals to show + + Returns + ------- + None + """ + + # Get number format and font from data labels + data_labels = plot.data_labels + number_format = data_labels.number_format # '0%' + font = data_labels.font + + plot_values = self.get_plot_values(plot) + for s, series in enumerate(plot_values): + values = [ + value + for value in series.values()[0] + if convertable(value, float) + ] + for v, value in enumerate(values): + if value is not None: + if number_format == '0%': + value = round(float(value) * 100, decimals) + + str_value = float2String(value) + '%' + else: + str_value = str(value) + else: + str_value = "" + point = plot.series[s].points[v] + data_label = point.data_label + frame = data_label.text_frame + frame.text = str_value + pgraph = frame.paragraphs[0] + for run in pgraph.runs: + run.font.bold = font.bold + # run.font.color.rgb = font.color.rgb + # run.font.fill.fore_color.rgb = font.fill.fore_color.rgb + + run.font.italic = font.italic + run.font.name = font.name + run.font.size = font.size + run.font.underline = font.underline + + def edit_datalabel(self, plot, series, point, text, prepend=False, append=False, rgb=None): + """ + Add/append data label text. + + Parameters + ---------- + plot: pptx.chart.plot._BasePlot + An instance of a Chart object. + serie: int + The serie where the data label should be edited + chart.series[serie] + point: int + The point where the data label should be edited + chart.series[serie].points[point] + text: basestring + The text to add/append to data label + prepend: bool + Set to True to prepend text to existing data label + append: bool + Set to True to append text to existing data label + rgb: tuple + Tuple with three ints defining each RGB color + + Returns + ------- + None + + """ + data_label = plot.series[series].points[point].data_label + frame = data_label.text_frame + + run = frame.paragraphs[0].runs[0] + original_text = frame.text + if prepend: + run.text = u'{}{}'.format(text, original_text) + elif append: + run.text = u'{}{}'.format(original_text, text) + else: + run.text = text + if rgb is not None: + run.font.color.rgb = RGBColor(*rgb) + def queue_slide_items(self, pptx_chain, slide_items): """ Helper function to queue a full automated slide. @@ -241,10 +368,28 @@ def queue_slide_items(self, pptx_chain, slide_items): side_table_draft['values_suffix_columns'] = pct_index self.queue_side_table(settings=side_table_draft) if slide_item.startswith('chart'): + sig_test = False cell_items = slide_item.split(':')[1] + + ''' + Makes no sense to actually have 'test' as a cell_item. + Will remove it from cell_items and set flag sig_test as True + ''' + cell_items = cell_items.split(',') + if 'test' in cell_items: + sig_test = True + pptx_chain.add_test_letter_to_column_labels() + pptx_chain.chart_df = pptx_chain.prepare_dataframe() + cell_items.remove('test') + cell_items = ','.join(cell_items) + pptx_frame = pptx_chain.chart_df.get(cell_items) if not pptx_frame().empty: chart_draft = self.draft_autochart(pptx_frame(), pptx_chain.chart_type) + if sig_test: + chart_draft['sig_test_visible'] = True + chart_draft['sig_test_results'] = pptx_chain.sig_test + self.queue_chart(settings=chart_draft) self._check_shapes() @@ -879,7 +1024,11 @@ def add_chart(self, slide, # Number format number_format='0.00%', - xl_number_format='0.00%' + xl_number_format='0.00%', + + # Sig test + sig_test_visible = False, + sig_test_results = None, ): """ Adds a chart to the given slide and sets all properties for the chart @@ -1044,6 +1193,27 @@ def add_chart(self, slide, data_labels.number_format = data_labels_num_format data_labels.number_format_is_linked = data_labels_num_format_is_linked + if not sig_test_results: sig_test_visible = False + if len(dataframe.columns) == 1: sig_test_visible = False + if sig_test_visible: + self.show_data_labels(plot, decimals=0) + for serie, column in enumerate(sig_test_results[::-1]): + for point, test_result in enumerate(column[::-1]): + if not isinstance(test_result, basestring): continue + for text in ['*.', + '*', + '**.', + '**', + '\'@L\'.', + '\'@L\'', + '\'@H\'.', + '\'@H\'', + ]: + test_result = test_result.replace(text,'') + if test_result == '': continue + text = u' ({})'.format(test_result) + self.edit_datalabel(plot, serie, point, text, prepend=False, append=True) + # # ================================ series # for i, ser in enumerate(dataframe.columns): # ser = plot.series[i] @@ -1416,85 +1586,3 @@ def add_net(slide, #paragraph.line_spacing = Pt(6) cell.text = str(subval) - @staticmethod - def edit_datalabel(chart, series, point, text, prepend=False, append=False, - position=None, rgb=None): - """ - Add/append data label text. - """ - data_label = chart.series[series].points[point].data_label - frame = data_label.text_frame - if prepend: - original_text = frame.text - frame.text = text - pgraph = frame.add_paragraph() - pgraph.text = original_text - run = frame.paragraphs[0].runs[0] - elif append: - pgraph = frame.add_paragraph() - pgraph.text = text - run = frame.paragraphs[-1].runs[0] - else: - frame.text = text - run = frame.paragraphs[0].runs[0] - if rgb is not None: - run.font.color.rgb = RGBColor(*rgb) - if position is not None: - data_label.position = data_label_pos_dct(position) - - def show_data_labels(self, chart, position=None): - """ - Explicitly sets datalabels to allow for datalabel editing. - - Parameters - ---------- - chart : pptx.chart.chart.Chart - The chart object for which datalabels need should be shown. - position : str, default=None - The position, relative to the data point, that the datalabel - should be appear in. Must be one of the following, or None: - 'above', 'below', 'best', 'center', 'inside_base', - 'inside_end', 'left', 'mixed', 'outside_end', 'right' - If None then the position already set for the chart will - be used. - - Returns - ------- - None - """ - - chart_values = self.get_chart_values(chart) - for s, series in enumerate(chart_values): - values = [ - value - for value in series.values()[0] - if self.convertable(value, float) - ] - for v, value in enumerate(values): - point = chart.series[s].points[v] - frame = point.data_label.text_frame - frame.text = "" if value is None else str(value) - if position is not None: - point.data_label.position = data_label_pos_dct(position) - - @staticmethod - def get_chart_values(chart): - - series = [ - {series.name: [str(s) for s in series.values]} - for series in chart.series - ] - - return series - - @staticmethod - def convertable(obj, func): - """ - Returns True if obj can be convertedby func without an error. - """ - - try: - func(obj) - return True - except ValueError: - return False From 80aa678ef73b7a1efa9b6bf1b0fedd159f0154bf Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 13 Feb 2019 11:37:15 +0100 Subject: [PATCH 656/733] get PptxChainClass.py up-to-date with staging-develop --- quantipy/sandbox/pptx/PptxChainClass.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 95754b0b4..036bbf95b 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -653,7 +653,13 @@ def sig_test(self): # Get the sig testing sig_df = self.prepare_dataframe() sig_df = sig_df.get_propstest() - self._sig_test = sig_df.df.values.tolist() + _sig_test = sig_df.df.values.tolist() + + # Assume that all items in the list of sig tests has same length + check_list = map(lambda x: len(x), _sig_test) + assert check_list.count(check_list[0]) == len(check_list), 'List of sig test results is not uniform' + + self._sig_test = [zip(*_sig_test)[i] for i in range(len(_sig_test[0]))] return self._sig_test @property From 2182096d44bdd0c3ab508c608d35e00d4669c598 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Wed, 13 Feb 2019 11:45:04 +0100 Subject: [PATCH 657/733] support for latest python-pptx package --- quantipy/sandbox/pptx/PptxPainterClass.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index a8aa050e1..6000f015b 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -13,7 +13,11 @@ Cm, Inches) -from pptx.shapes import table +try: + from pptx import table +except: + from pptx.shapes import table + from pptx.chart.data import ChartData from pptx.enum.chart import XL_CHART_TYPE from pptx.dml.color import RGBColor From 837272a23dde5858e34a4048ceaa8e751e47c72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 14 Feb 2019 15:33:11 +0100 Subject: [PATCH 658/733] new function DataSet.used_text_keys --- quantipy/core/dataset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..ba8865fee 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -5637,6 +5637,22 @@ def extend_valid_tks(self, new_tk): self._meta['lib']['valid text'] = self.valid_tks return None + @staticmethod + def _used_text_keys(text_dict, tks): + new = [tk for tk in text_dict.keys() + if not tk in ['x edits', 'y edits'] + tks['tks']] + tks['tks'] += new + + def used_text_keys(self): + """ + Get a list of all used textkeys in the dataset instance. + """ + text_func = self._used_text_keys + args = () + kwargs = {'tks': {'tks': []}} + DataSet._apply_to_texts(text_func, self._meta, args, kwargs) + return kwargs['tks']['tks'] + @staticmethod def _force_texts(text_dict, copy_to, copy_from, update_existing): new_text_key = None From ce085e3cdf71b256bca2efccf2e574abd8c67d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 19 Feb 2019 11:20:57 +0100 Subject: [PATCH 659/733] switch to en-GB for unknown tks --- quantipy/core/dataset.py | 2 +- quantipy/sandbox/sandbox.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..d4e916ebd 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -50,7 +50,7 @@ VALID_TKS = [ 'en-GB', 'da-DK', 'fi-FI', 'nb-NO', 'sv-SE', 'de-DE', 'fr-FR', 'ar-AR', - 'es-ES', 'it-IT'] + 'es-ES', 'it-IT', 'pl-PL'] VAR_SUFFIXES = [ '_rc', '_net', ' (categories', ' (NET', '_rec'] diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 520b4b402..2737ed4a3 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1912,7 +1912,8 @@ def clone(self): @lazy_property def _default_text(self): - return self._meta['lib']['default text'] + tk = self._meta['lib']['default text'] + return tk if tk in self._transl else 'en-GB' @lazy_property def orientation(self): From 22033943555af56f68baf5964f10d25319c8206f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 19 Feb 2019 14:20:09 +0100 Subject: [PATCH 660/733] handle default text_keys in paint --- quantipy/sandbox/sandbox.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 2737ed4a3..8bcc69fec 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1913,7 +1913,9 @@ def clone(self): @lazy_property def _default_text(self): tk = self._meta['lib']['default text'] - return tk if tk in self._transl else 'en-GB' + if tk not in self._transl: + self._transl[tk] = self._transl['en-GB'] + return tk @lazy_property def orientation(self): From c372b2e3ab8a6f0e485f3334a9554ffc6fe335ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 22 Feb 2019 11:15:17 +0100 Subject: [PATCH 661/733] reindex data before filter is applied --- quantipy/core/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..c95f0b690 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6943,6 +6943,7 @@ def filter(self, alias, condition, inplace=False): Filter the DataSet using a Quantipy logical expression. """ data = self._data.copy() + data.index = pd.Index(range(0, len(data.index))) filter_idx, _ = get_logic_index(pd.Series(data.index), condition, data) filtered_data = data.iloc[filter_idx, :] if inplace: From 6c46bcda9dde499795c0588b610e809c3ab8d9b4 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Tue, 26 Feb 2019 12:57:54 +0100 Subject: [PATCH 662/733] change table frame to str and support comma as dec separator --- quantipy/sandbox/pptx/PptxChainClass.py | 35 ++++++++++++----------- quantipy/sandbox/pptx/PptxPainterClass.py | 12 ++++++-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/quantipy/sandbox/pptx/PptxChainClass.py b/quantipy/sandbox/pptx/PptxChainClass.py index 036bbf95b..cfe8f2ddc 100644 --- a/quantipy/sandbox/pptx/PptxChainClass.py +++ b/quantipy/sandbox/pptx/PptxChainClass.py @@ -293,7 +293,7 @@ def __init__(self, dataframe, cell_types, array_style): def __call__(self): return self.df - def to_table(self, decimals=2, pct_decimals=2): + def to_table(self, decimals=2, pct_decimals=2, decimal_separator='.'): """ Returns self.df formatted to be added to a table in a slide. Basically just rounds values and if cell type = % then multiply values with 100 @@ -305,6 +305,8 @@ def to_table(self, decimals=2, pct_decimals=2): Number of decimals for not percentage cell_types pct_decimals: int Number of decimals for percentage cell_types + decimal_separator: str + Returns ------- self @@ -317,26 +319,27 @@ def to_table(self, decimals=2, pct_decimals=2): if self.array_style == -1: df = df.T - rows_not_nan = pd.notnull(df).any(axis='columns') - df = df.fillna('') # Percent type cells - indexes = get_indexes_from_list(self.cell_items, PCT_TYPES, exact=False) - df.iloc[:, indexes] *= 100 - df.iloc[:, indexes] = df.iloc[:, indexes].round(decimals=pct_decimals) - if pct_decimals == 0: - columns = df.columns[indexes].tolist() - columns_to_int=dict(zip(columns, ['int'] * len(columns))) - df[rows_not_nan] = df[rows_not_nan].astype(columns_to_int) + pct_indexes = get_indexes_from_list(self.cell_items, PCT_TYPES, exact=False) + df.iloc[:, pct_indexes] *= 100 + df.iloc[:, pct_indexes] = df.iloc[:, pct_indexes].round(decimals=pct_decimals) # Not percent type cells - indexes = get_indexes_from_list(self.cell_items, NOT_PCT_TYPES, exact=False) - df.iloc[:, indexes] = df.iloc[:, indexes].round(decimals=decimals) - if decimals == 0: - columns = df.columns[indexes].tolist() - columns_to_int=dict(zip(columns, ['int'] * len(columns))) - df[rows_not_nan] = df[rows_not_nan].astype(columns_to_int) + not_pct_indexes = get_indexes_from_list(self.cell_items, NOT_PCT_TYPES, exact=False) + df.iloc[:, not_pct_indexes] = df.iloc[:, not_pct_indexes].round(decimals=decimals) + + df = df.astype('str') + + if pct_decimals == 0 or decimals == 0: + pct_columns = df.columns[pct_indexes].tolist() if pct_decimals == 0 else [] + not_pct_columns = df.columns[not_pct_indexes].tolist() if decimals == 0 else [] + columns = pct_columns + not_pct_columns + df[columns] = df[columns].replace('\.0', '', regex=True) + + if not decimal_separator == '.': + df = df.replace('\.', ',', regex=True) if self.array_style == -1: df = df.T diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index 6000f015b..7df4971b8 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -300,7 +300,7 @@ def edit_datalabel(self, plot, series, point, text, prepend=False, append=False, if rgb is not None: run.font.color.rgb = RGBColor(*rgb) - def queue_slide_items(self, pptx_chain, slide_items): + def queue_slide_items(self, pptx_chain, slide_items, decimal_separator='.', pct_decimals=0, decimals=2): """ Helper function to queue a full automated slide. Includes queueing of header with question text, a table or chart with optional side table, @@ -357,13 +357,19 @@ def queue_slide_items(self, pptx_chain, slide_items): for slide_item in slide_items: if slide_item.startswith('table'): cell_items = slide_item.split(':')[1] - pptx_frame = pptx_chain.chart_df.get(cell_items).to_table(pct_decimals=0) + pptx_frame = pptx_chain.chart_df.get(cell_items).to_table(pct_decimals=pct_decimals, + decimals=decimals, + decimal_separator=decimal_separator, + ) if not pptx_frame().empty: table_draft = self.draft_table(pptx_frame()) self.queue_table(settings=table_draft) if slide_item.startswith('side_table'): cell_items = slide_item.split(':')[1] - pptx_frame = pptx_chain.chart_df.get(cell_items).to_table(pct_decimals=0) + pptx_frame = pptx_chain.chart_df.get(cell_items).to_table(pct_decimals=pct_decimals, + decimals=decimals, + decimal_separator=decimal_separator, + ) if not pptx_frame().empty: side_table_draft = self.draft_side_table(pptx_frame()) pct_index = [index for index, value in enumerate(pptx_frame.cell_items) if 'is_c_pct' in value] From ee9bebd5eb10c6b9de7ce0eb55bb4ac3f25a64d3 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Tue, 26 Feb 2019 13:25:12 +0100 Subject: [PATCH 663/733] just formatting --- quantipy/sandbox/pptx/PptxPainterClass.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/pptx/PptxPainterClass.py b/quantipy/sandbox/pptx/PptxPainterClass.py index 7df4971b8..9980a01a1 100644 --- a/quantipy/sandbox/pptx/PptxPainterClass.py +++ b/quantipy/sandbox/pptx/PptxPainterClass.py @@ -300,7 +300,11 @@ def edit_datalabel(self, plot, series, point, text, prepend=False, append=False, if rgb is not None: run.font.color.rgb = RGBColor(*rgb) - def queue_slide_items(self, pptx_chain, slide_items, decimal_separator='.', pct_decimals=0, decimals=2): + def queue_slide_items(self, pptx_chain, slide_items, + decimal_separator='.', + pct_decimals=0, + decimals=2, + ): """ Helper function to queue a full automated slide. Includes queueing of header with question text, a table or chart with optional side table, From 1e4050527b6ba3832ba762912e40fa3914908157 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 27 Feb 2019 09:16:00 +0100 Subject: [PATCH 664/733] crossbreak nesting now supported in the _reindex() method --- quantipy/sandbox/sandbox.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index a9e1f9076..2bd3987b3 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1802,8 +1802,19 @@ def __init__(self, chain): self._org_col.get_level_values(1))} self.df.columns = self._edit_col self.array_mi = c._array_style == 0 + self.nested_y = c._nested_y + self._nest_mul = self._nesting_multiplier() return None + def _nesting_multiplier(self): + """ + """ + levels = self._org_col.nlevels + if levels == 2: + return 1 + else: + return (levels / 2) + 1 + def _insert_viewlikes(self, new_index_flat, org_index_mapped): inserts = [new_index_flat.index(val) for val in new_index_flat if not val in org_index_mapped.values()] @@ -1844,7 +1855,9 @@ def _updated_index_tuples(self, axis): if org_tuples[idx][1] == merged_val: new_tuples.append(org_tuples[idx]) else: - new_tuples.append(('*', merged_val)) + empties = ['*'] * self._nest_mul + new_tuple = tuple(empties + [merged_val]) + new_tuples.append(new_tuple) d += 1 i += 1 return new_tuples @@ -1857,6 +1870,7 @@ def _reindex(self): x_names = y_names else: x_names = ['Array', 'Questions'] + if self.nested_y: y_names = y_names * (self._nest_mul - 1) tuples = self._updated_index_tuples(axis=1) self.df.columns = pd.MultiIndex.from_tuples(tuples, names=y_names) tuples = self._updated_index_tuples(axis=0) From eec44987915c91a37bc522398a19af4e2f8c1aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 14 Mar 2019 10:06:25 +0100 Subject: [PATCH 665/733] remove print --- quantipy/core/dataset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..128d10573 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -6681,7 +6681,6 @@ def set_missings(self, var, missing_map='default', hide_on_y=True, else: self._meta['columns'][v]['missings'] = v_m_map if hide_on_y: - print missing_map self.hiding(var, missing_map, 'y', True) return None From b99f0af0aab0e84fc9eaf6425a14144370dedf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 14 Mar 2019 13:24:23 +0100 Subject: [PATCH 666/733] nested in unroll and decorator --- quantipy/core/batch.py | 10 +++++----- quantipy/core/dataset.py | 27 +++++++++++++++++---------- quantipy/core/tools/qp_decorators.py | 21 ++++++++++++++++++--- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 288cab552..e566d0131 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -606,7 +606,7 @@ def codes_in_data(self, name): else: data_codes = pd.get_dummies(data).columns.tolist() return data_codes - + def hide_empty(self, xks=True, summaries=True): """ Drop empty variables and hide array items from summaries. @@ -743,7 +743,7 @@ def add_total(self, total=True): return None @modify(to_list='xbrk') - @verify(variables={'xbrk': 'both'}, categorical='xbrk') + @verify(variables={'xbrk': 'both_nested'}, categorical='xbrk') def add_crossbreak(self, xbrk): """ Set the y (crossbreak/banner) variables of the Batch. @@ -767,7 +767,7 @@ def add_crossbreak(self, xbrk): return None @modify(to_list='yks') - @verify(variables={'yks': 'both'}, categorical='yks') + @verify(variables={'yks': 'both_nested'}, categorical='yks') def add_y(self, yks): """ Set the y (crossbreak/banner) variables of the Batch. @@ -936,7 +936,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): return None @modify(to_list=['ext_yks', 'on']) - @verify(variables={'ext_yks': 'columns'}) + @verify(variables={'ext_yks': 'columns_nested'}) def extend_y(self, ext_yks, on=None): """ Add y (crossbreak/banner) variables to specific x (downbreak) variables. @@ -979,7 +979,7 @@ def extend_y(self, ext_yks, on=None): return None @modify(to_list=['new_yks', 'on']) - @verify(variables={'new_yks': 'both', 'on': 'both'}) + @verify(variables={'new_yks': 'both_nested', 'on': 'both'}) def replace_y(self, new_yks, on): """ Replace y (crossbreak/banner) variables on specific x (downbreak) variables. diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 128d10573..b7805e47c 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2947,7 +2947,7 @@ def roll_up(self, varlist, ignore_arrays=None): return rolled_up @modify(to_list=['varlist', 'keep', 'both']) - @verify(variables={'varlist': 'both', 'keep': 'masks'}) + @verify(variables={'varlist': 'both_nested', 'keep': 'masks'}) def unroll(self, varlist, keep=None, both=None): """ Replace mask with their items, optionally excluding/keeping certain ones. @@ -2967,19 +2967,26 @@ def unroll(self, varlist, keep=None, both=None): unrolled : list The modified ``varlist``. """ + def _unroll(unrolled_v, v, keep, both): + if not self.is_array(v) or keep: + unrolled_v.append(v) + else: + if both: + unrolled_v.append(v) + unrolled_v.extend(self.sources(v)) + return unrolled_v + if both and both[0] == 'all': - both = [mask for mask in varlist if mask in self._meta['masks']] + both = self.masks() unrolled = [] for var in varlist: - if not self.is_array(var): - unrolled.append(var) + if ' > ' in var: + nested = var.replace(' ', '').split('>') + unrolled_nested = product(*[ + _unroll([], n, n in keep, n in both) for n in nested]) + unrolled.extend([' > '.join(list(un)) for un in unrolled_nested]) else: - if not var in keep: - if var in both: - unrolled.append(var) - unrolled.extend(self.sources(var)) - else: - unrolled.append(var) + unrolled = _unroll(unrolled, var, var in keep, var in both) return unrolled def _apply_order(self, variables): diff --git a/quantipy/core/tools/qp_decorators.py b/quantipy/core/tools/qp_decorators.py index c427c85fe..71f39a7e6 100644 --- a/quantipy/core/tools/qp_decorators.py +++ b/quantipy/core/tools/qp_decorators.py @@ -26,6 +26,10 @@ def _var_in_ds(func, *args, **kwargs): all_args = getargspec(func)[0] ds = args[0] for variable, collection in variables.items(): + nested = False + if collection.endswith('_nested'): + nested = True + collection = collection.split('_')[0] # get collection for argument if collection == 'both': collection = ['columns', 'masks'] @@ -35,10 +39,21 @@ def _var_in_ds(func, *args, **kwargs): # get the variable argument to check v_index = all_args.index(variable) var = kwargs.get(variable, args[v_index]) - if var is None: return func(*args, **kwargs) - if not isinstance(var, list): var = [var] + if var is None: + return func(*args, **kwargs) + if not isinstance(var, list): + var = [var] + if nested: + valid = [] + for v in var: + if ' > ' in v: + valid.extend(v.replace(' ', '').split('>')) + else: + valid.append(v) + else: + valid = var # check the variable - not_valid = [v for v in var if not v in c + ['@']] + not_valid = [v for v in valid if not v in c + ['@']] if not_valid: msg = "'{}' argument for {}() must be in {}.\n" msg += '{} is not in {}.' From f55bd49e6413d47f8609e86f319529f3a841c6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 14 Mar 2019 13:35:27 +0100 Subject: [PATCH 667/733] bugfix in drop arrays, keep non categorical items --- quantipy/core/dataset.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..6da498d64 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3957,9 +3957,10 @@ def remove_loop(obj, var): meta['sets']['data file']['items'] = n_items if self._has_categorical_data(var): values = meta['lib']['values'][var] - for source in self.sources(var): + for source in self.sources(var): + if self._has_categorical_data(var): meta['columns'][source]['values'] = values - meta['columns'][source]['parent'] = {} + meta['columns'][source]['parent'] = {} df_items = meta['sets']['data file']['items'] n_items = [i for i in df_items if not i.split('@')[-1] in name] From 03e1072627d71ce5591ee6725ce03aac90630129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 14 Mar 2019 15:41:54 +0100 Subject: [PATCH 668/733] add nesting in roll_up and decorators --- quantipy/core/dataset.py | 116 +++++++++++++++++++-------- quantipy/core/tools/qp_decorators.py | 9 ++- 2 files changed, 89 insertions(+), 36 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index b7805e47c..14e9d1507 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2905,8 +2905,23 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', # ------------------------------------------------------------------------ # lists/ sets of variables/ data file items # ------------------------------------------------------------------------ - @modify(to_list=['varlist']) - @verify(variables={'varlist': 'both'}) + + def _array_and_item_list(self, v, keep): + new_list = [] + if not self.is_array(v): + # columns + if keep in ['both', 'items']: + new_list.append(v) + else: + # masks + if keep in ['both', 'mask']: + new_list.append(v) + if keep in ['both', 'items']: + new_list.extend(self.sources(v)) + return new_list + + @modify(to_list=['varlist', 'ignore_arrays']) + @verify(variables={'varlist': 'both_nested', 'ignore_arrays': 'masks'}) def roll_up(self, varlist, ignore_arrays=None): """ Replace any array items with their parent mask variable definition name. @@ -2918,32 +2933,46 @@ def roll_up(self, varlist, ignore_arrays=None): ignore_arrays : (list of) str A list of array mask names that should not be rolled up if their items are found inside ``varlist``. + + Note + ---- + varlist can also contain nesting `var1 > var2`. The variables which are + included in the nesting can also be controlled by keep and both, even + if the variables are also included as a "normal" variable. + Returns ------- rolled_up : list The modified ``varlist``. """ - if ignore_arrays: - if not isinstance(ignore_arrays, list): - ignore_arrays = [ignore_arrays] - else: - ignore_arrays = [] - arrays_defs = {arr: self.sources(arr) for arr in self.masks() - if not arr in ignore_arrays} - item_map = {} - for k, v in arrays_defs.items(): - for item in v: - item_map[item] = k + def _var_to_keep(var, ignore): + if self.is_array(var): + to_keep = 'mask' + else: + to_keep = 'items' + if self._is_array_item(var): + parent = self._maskname_from_item(var) + if parent not in ignore_arrays: + var = parent + to_keep = 'mask' + return var, to_keep + rolled_up = [] - for v in varlist: - if not self.is_array(v): - if v in item_map: - if not item_map[v] in rolled_up: - rolled_up.append(item_map[v]) - else: - rolled_up.append(v) + for var in varlist: + if ' > ' in var: + nested = var.replace(' ', '').split('>') + n_list = [] + for n in nested: + n, to_keep = _var_to_keep(n, ignore_arrays) + n_list.append(self._array_and_item_list(n, to_keep)) + for ru in [' > '.join(list(un)) for un in product(*n_list)]: + if ru not in rolled_up: + rolled_up.append(ru) else: - rolled_up.append(v) + var, to_keep = _var_to_keep(var, ignore_arrays) + for ru in self._array_and_item_list(var, to_keep): + if ru not in rolled_up: + rolled_up.append(ru) return rolled_up @modify(to_list=['varlist', 'keep', 'both']) @@ -2962,31 +2991,50 @@ def unroll(self, varlist, keep=None, both=None): The names of masks that will be included both as themselves and as collections of their items. + Note + ---- + varlist can also contain nesting `var1 > var2`. The variables which are + included in the nesting can also be controlled by keep and both, even + if the variables are also included as a "normal" variable. + + Example:: + >>> ds.unroll(varlist = ['q1', 'q1 > gender'], both='all') + ['q1', + 'q1_1', + 'q1_2', + 'q1 > gender', + 'q1_1 > gender', + 'q1_2 > gender'] + Returns ------- unrolled : list The modified ``varlist``. """ - def _unroll(unrolled_v, v, keep, both): - if not self.is_array(v) or keep: - unrolled_v.append(v) - else: - if both: - unrolled_v.append(v) - unrolled_v.extend(self.sources(v)) - return unrolled_v - if both and both[0] == 'all': both = self.masks() unrolled = [] for var in varlist: if ' > ' in var: nested = var.replace(' ', '').split('>') - unrolled_nested = product(*[ - _unroll([], n, n in keep, n in both) for n in nested]) - unrolled.extend([' > '.join(list(un)) for un in unrolled_nested]) + n_list = [] + for n in nested: + if n in keep: + to_keep = 'mask' + elif n in both: + to_keep = 'both' + else: + to_keep = 'items' + n_list.append(self._array_and_item_list(n, to_keep)) + unrolled.extend([' > '.join(list(un)) for un in product(*n_list)]) else: - unrolled = _unroll(unrolled, var, var in keep, var in both) + if var in keep: + to_keep = 'mask' + elif var in both: + to_keep = 'both' + else: + to_keep = 'items' + unrolled += self._array_and_item_list(var, to_keep) return unrolled def _apply_order(self, variables): diff --git a/quantipy/core/tools/qp_decorators.py b/quantipy/core/tools/qp_decorators.py index 71f39a7e6..3831dc996 100644 --- a/quantipy/core/tools/qp_decorators.py +++ b/quantipy/core/tools/qp_decorators.py @@ -72,9 +72,14 @@ def _var_is_cat(func, *args, **kwargs): var = kwargs.get(cat, args[v_index]) if var is None: return func(*args, **kwargs) if not isinstance(var, list): var = [var] - var = [v for v in var if not v == '@'] + valid = [] + for v in var: + if ' > ' in v: + valid.extend(v.replace(' ', '').split('>')) + elif not '@' == v: + valid.append(v) # check if varaibles are categorical - not_cat = [v for v in var if not ds._has_categorical_data(v)] + not_cat = [v for v in valid if not ds._has_categorical_data(v)] if not_cat: msg = "'{}' argument for {}() must reference categorical " msg += 'variable.\n {} is not categorical.' From cab940fdba4c7c990ca8fdcbbd81f786e1c72a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 15 Mar 2019 11:36:22 +0100 Subject: [PATCH 669/733] batch level + transpose and clean up --- .pytest_cache/v/cache/nodeids | 3 +- quantipy/core/batch.py | 252 ++++++++++++++-------------------- quantipy/core/dataset.py | 8 +- quantipy/core/stack.py | 2 +- tests/test_batch.py | 106 ++++++++------ 5 files changed, 177 insertions(+), 194 deletions(-) diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index fc4d323c1..203477c9a 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -16,7 +16,7 @@ "tests/test_batch.py::TestBatch::test_extend_y", "tests/test_batch.py::TestBatch::test_from_batch", "tests/test_batch.py::TestBatch::test_hiding", - "tests/test_batch.py::TestBatch::test_make_summaries_transpose_arrays", + "tests/test_batch.py::TestBatch::test_level", "tests/test_batch.py::TestBatch::test_replace_y", "tests/test_batch.py::TestBatch::test_set_cell_items", "tests/test_batch.py::TestBatch::test_set_language", @@ -24,6 +24,7 @@ "tests/test_batch.py::TestBatch::test_set_weight", "tests/test_batch.py::TestBatch::test_slicing", "tests/test_batch.py::TestBatch::test_sorting", + "tests/test_batch.py::TestBatch::test_transpose", "tests/test_chain.py::TestChainConstructor::()::test_init", "tests/test_chain.py::TestChainConstructor::()::test_str", "tests/test_chain.py::TestChainConstructor::()::test_repr", diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index e566d0131..12d7d09a5 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -131,7 +131,6 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self._variables = [] self._section_starts = {} self.total = True - self.extended_yks_global = None self.extended_yks_per_x = {} self.exclusive_yks_per_x = {} self.extended_filters_per_x = {} @@ -143,8 +142,10 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.y_on_y_filter = {} self.y_filter_map = {} self.forced_names = {} - self.summaries = [] - self.transposed_arrays = {} + self.transposed = [] + self.leveled = {} + # self.summaries = [] + # self.transposed_arrays = {} self.skip_items = [] self.verbatims = [] # self.verbatim_names = [] @@ -184,8 +185,8 @@ def _update(self): attrs = self.__dict__ for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', @@ -202,8 +203,8 @@ def _load_batch(self): bdefs = self._meta['sets']['batches'][self.name] for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', @@ -423,7 +424,6 @@ def as_addition(self, batch_name): self._update() return None - def as_main(self, keep=True): """ Transform additional ``Batch`` definitions into regular (parent/main) ones. @@ -472,7 +472,6 @@ def add_variables(self, varlist): self._update() return None - @modify(to_list='dbrk') def add_downbreak(self, dbrk): """ @@ -492,28 +491,16 @@ def add_downbreak(self, dbrk): clean_xks = self._check_forced_names(dbrk) self.xks = self.unroll(clean_xks, both='all') self._update() - masks = [x for x in self.xks if x in self.masks()] - self.make_summaries(masks, [], _verbose=False) return None - @modify(to_list='xks') def add_x(self, xks): """ - Set the x (downbreak) variables of the Batch. - - Parameters - ---------- - xks: str, list of str, dict, list of dict - Names of variables that are used as downbreaks. Forced names for - Excel outputs can be given in a dict, for example: - xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] + Deprecated! Set the x (downbreak) variables of the Batch. - Returns - ------- - None """ - w = "'add_x()' will be deprecated in a future version. Please use 'add_downbreak()' instead!" + w = ("'add_x()' will be deprecated in a future version. " + "Please use 'add_downbreak()' instead!") warnings.warn(w) self.add_downbreak(xks) @@ -540,25 +527,24 @@ def extend_x(self, ext_xks): for x in ext_xks: if isinstance(x, dict): for pos, var in x.items(): + if pos not in self: + raise KeyError('{} is not included.'.format(pos)) + elif self._is_array_item(pos): + msg = '{}: Cannot use an array item as position' + raise ValueError(msg.format(pos)) if not isinstance(var, list): var = [var] - var = self.unroll(var, both='all') for v in var: - if not self.var_exists(pos): - raise KeyError('{} is not included.'.format(pos)) - elif not v in self.xks: - self.xks.insert(self.xks.index(pos), v) - if self.is_array(v) and not v in self.summaries: - self.summaries.append(v) - elif not self.var_exists(x): - raise KeyError('{} is not included.'.format(x)) - elif x not in self.xks: - self.xks.extend(self.unroll(x, both='all')) - if self.is_array(x) and not x in self.summaries: - self.summaries.append(x) + if v in self.xks: + msg = '{} is already included as downbreak.' + raise ValueError(msg.format(v)) + self.xks.insert(self.xks.index(pos), v) + else: + if x not in self: + raise KeyError('{} is not included.'.format(x)) + self.xks.append(x) self._update() return None - def add_section(self, x_anchor, section): """ """ @@ -636,7 +622,6 @@ def hide_empty(self, xks=True, summaries=True): self.hiding(x, e_items, axis='x', hide_values=False) if len(e_items) == len(sources): if x in self.xks: self.xks.remove(x) - if x in self.summaries: self.summaries.remove(x) removed_sum.append(x) if xks: for i in e_items: @@ -655,79 +640,77 @@ def hide_empty(self, xks=True, summaries=True): self._update() return None - @modify(to_list=['arrays']) - @verify(variables={'arrays': 'masks'}) def make_summaries(self, arrays, exclusive=False, _verbose=None): """ - Summary tables are created for defined arrays. + Deprecated! Summary tables are created for defined arrays. + """ + msg = ("Depricated! `make_summaries()` is not available anymore, please" + " use `exclusive_arrays()` to skip items.") + raise NotImplementedError(msg) - Parameters - ---------- - arrays: str/ list of str - List of arrays for which summary tables are created. Summary tables - can only be created for arrays that are included in ``self.xks``. - exclusive: bool/ list, default False - If True only summaries are created and items skipped. ``exclusive`` - parameter can be provided for a selection of arrays. Example:: - >>> b.make_summaries(['array1', 'array2'], exclusive = ['array2']) - Returns - ------- - None + def transpose_arrays(self, arrays, replace=False): """ - if _verbose is None: _verbose = self._verbose_infos - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - self.summaries = arrays - if exclusive: - if isinstance(exclusive, bool): - self.skip_items = arrays - else: - self.skip_items = [a for a in exclusive if a in arrays] - else: - self.skip_items = [] - if arrays: - msg = 'Array summaries setup: Creating {}.'.format(arrays) - else: - msg = 'Array summaries setup: Creating no summaries!' - if _verbose: - print msg - for t_array in self.transposed_arrays.keys(): - if not t_array in arrays: - self.transposed_arrays.pop(t_array) + Deprecated! Transposed summary tables are created for defined arrays. + """ + msg = ("Depricated! `transpose_arrays()` is not available anymore, " + "please use `exclusive_arrays()` to skip items.") + raise NotImplementedError(msg) + + @modify(to_list=['array']) + @verify(variables={'array': 'masks'}) + def exclusive_arrays(self, array): + """ + For defined arrays only summary tables are produced. Items get ignored. + """ + not_valid = [a for a in array if a not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + self.skip_items = array self._update() return None - @modify(to_list='arrays') - @verify(variables={'arrays': 'masks'}) - def transpose_arrays(self, arrays, replace=False): + @modify(to_list='name') + @verify(variables={'name': 'both'}) + def transpose(self, name): """ - Transposed summary tables are created for defined arrays. + Create transposed aggregations for the requested variables. Parameters ---------- - arrays: str/ list of str - List of arrays for which transposed summary tables are created. - Transposed summary tables can only be created for arrays that are - included in ``self.xks``. - replace: bool, default True - If True only the transposed table is created, if False transposed - and normal summary tables are created. + name: str/ list of str + Name of variable(s) for which transposed aggregations will be + created. + """ + not_valid = [n for n in name if n not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + self.transposed = name + self._update() + return None - Returns - ------- - None + @modify(to_list='array') + @verify(variables={'array': 'masks'}) + def level(self, array, levels={}): """ - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - if any(a not in self.summaries for a in arrays): - ar = list(set(self.summaries + arrays)) - a = [v for v in self.xks if v in ar] - self.make_summaries(a, []) - for array in arrays: - self.transposed_arrays[array] = replace - self._update() + Level arrays by defined level variables. + + Parameters + ---------- + array: str/ list of str + Names of the arrays to add the levels to. + levels: list/ dict of lists + The levels which should be used for the specific arrays. + """ + not_valid = [a for a in array if a not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + for a in array: + if not isinstance(levels, list): + l = levels.get(a) or self.yks + else: + l = levels + self.leveled[a] = l + self._update() return None def add_total(self, total=True): @@ -770,38 +753,13 @@ def add_crossbreak(self, xbrk): @verify(variables={'yks': 'both_nested'}, categorical='yks') def add_y(self, yks): """ - Set the y (crossbreak/banner) variables of the Batch. - - Parameters - ---------- - yks: str, list of str - Variables that are added as crossbreaks. '@'/ total is added - automatically. - - Returns - ------- - None + Deprecated! Set the y (crossbreak/banner) variables of the Batch. """ - w = "'add_y()' will be deprecated in a future version. Please use 'add_crossbreak()' instead!" + w = ("'add_y()' will be deprecated in a future version. Please use" + " 'add_crossbreak()' instead!") warnings.warn(w) self.add_crossbreak(yks) - def add_x_per_y(self, x_on_y_map): - """ - Add individual combinations of x and y variables to the Batch. - - !!! Currently not implemented !!! - """ - raise NotImplementedError('NOT YET SUPPPORTED') - if not isinstance(x_on_y_map, list): x_on_y_maps = [x_on_y_map] - if not isinstance(x_on_y_maps[0], dict): - raise TypeError('Must pass a (list of) dicts!') - for x_on_y_map in x_on_y_maps: - for x, y in x_on_y_map.items(): - if not isinstance(y, list): y = [y] - if isinstance(x, tuple): x = {x[0]: x[1]} - return None - def add_filter(self, filter_name, filter_logic=None, overwrite=False): """ Apply a (global) filter to all the variables found in the Batch. @@ -936,7 +894,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): return None @modify(to_list=['ext_yks', 'on']) - @verify(variables={'ext_yks': 'columns_nested'}) + @verify(variables={'ext_yks': 'both_nested'}) def extend_y(self, ext_yks, on=None): """ Add y (crossbreak/banner) variables to specific x (downbreak) variables. @@ -959,23 +917,18 @@ def extend_y(self, ext_yks, on=None): ------- None """ - ext_yks = [e for e in ext_yks if not e in self.yks] if not on: - self.yks.extend(ext_yks) - if not self.extended_yks_global: - self.extended_yks_global = ext_yks - else: - self.extended_yks_global.extend(ext_yks) + self.add_crossbreak(self.yks + ext_yks) else: - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + not_valid = [o for o in on if not o in self.xks] + if not_valid: + msg = '{} not defined as xks.'.format(not_valid) raise ValueError(msg) - on = self.unroll(on, both='all') + on = self.unroll(on) for x in on: - x_ext_yks = [e for e in ext_yks - if not e in self.extended_yks_per_x.get(x, [])] - self.extended_yks_per_x.update({x: x_ext_yks}) - self._update() + x_ext = self.unroll(self.extended_yks_per_x.get(x, []) + ext_yks) + self.extended_yks_per_x.update({x: x_ext}) + self._update() return None @modify(to_list=['new_yks', 'on']) @@ -996,11 +949,13 @@ def replace_y(self, new_yks, on): ------- None """ - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + not_valid = [o for o in on if not o in self.xks] + if not_valid: + msg = '{} not defined as xks.'.format(not_valid) raise ValueError(msg) - on = self.unroll(on, both='all') - if not '@' in new_yks: new_yks = ['@'] + new_yks + on = self.unroll(on) + if not '@' in new_yks and self.total: + new_yks = ['@'] + new_yks for x in on: self.exclusive_yks_per_x.update({x: new_yks}) self._update() @@ -1108,8 +1063,7 @@ def _get_yks(x): mapping = [] for x in self.xks: if self.is_array(x): - if x in self.summaries and not self.transposed_arrays.get(x): - mapping.append((x, ['@'])) + mapping.append((x, self.leveled.get(x, ['@']))) if not x in self.skip_items: try: hiding = self.meta_edits[x]['rules']['x']['dropx']['values'] @@ -1120,12 +1074,12 @@ def _get_yks(x): continue elif x2 in self.xks: mapping.append((x2, _get_yks(x2))) - if x in self.transposed_arrays: - mapping.append(('@', [x])) elif self._is_array_item(x) and self._maskname_from_item(x) in self.xks: continue else: mapping.append((x, _get_yks(x))) + if x in self.transposed: + mapping.append(('@', [x])) self.x_y_map = mapping return None diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 14e9d1507..2c1642d14 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3026,7 +3026,9 @@ def unroll(self, varlist, keep=None, both=None): else: to_keep = 'items' n_list.append(self._array_and_item_list(n, to_keep)) - unrolled.extend([' > '.join(list(un)) for un in product(*n_list)]) + for ur in [' > '.join(list(un)) for un in product(*n_list)]: + if ur not in unrolled: + unrolled.append(ur) else: if var in keep: to_keep = 'mask' @@ -3034,7 +3036,9 @@ def unroll(self, varlist, keep=None, both=None): to_keep = 'both' else: to_keep = 'items' - unrolled += self._array_and_item_list(var, to_keep) + for ur in self._array_and_item_list(var, to_keep): + if ur not in unrolled: + unrolled.append(ur) return unrolled def _apply_order(self, variables): diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index d65bc59b0..5b14689a4 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -738,7 +738,6 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " "at the same time." ) - x = self._force_key_as_list(x) y = self._force_key_as_list(y) @@ -1972,6 +1971,7 @@ def aggregate(self, views, unweighted_base=True, categorize=[], xs = [x for x in x_y_f_w_map.keys() if x in x_in_stack] else: xs = [x for x in xs if x in x_in_stack or isinstance(x, tuple)] + v_typ = self.variable_types(dk, verbose=False) numerics = v_typ['int'] + v_typ['float'] masks = self[dk].meta['masks'] diff --git a/tests/test_batch.py b/tests/test_batch.py index 0291bd8f1..3ffb3b50d 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -49,7 +49,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 32) + self.assertEqual(len(_get_meta(batch1).keys()), 31) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) @@ -60,13 +60,16 @@ def test_dataset_get_batch(self): batch, ds = _get_batch('test', full=True) self.assertRaises(KeyError, ds.get_batch, 'name') b = ds.get_batch('test') - attr = ['xks', 'yks', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'name', 'total'] + attr = [ + 'xks', 'yks', '_variables', 'filter', 'filter_names', + 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language', 'name', 'skip_items', 'total', + 'unwgt_counts', 'y_filter_map', 'build_info', + '_section_starts'] for a in attr: self.assertEqual(batch.__dict__[a], b.__dict__[a]) @@ -103,7 +106,6 @@ def test_add_downbreak(self): u'q14r03c01', u'q14r04c01', u'q14r05c01', u'q14r06c01', u'q14r07c01', u'q14r08c01', u'q14r09c01', u'q14r10c01']) self.assertEqual(b_meta['forced_names'], {'q3': 'q3_label', 'q5': 'q5_label'}) - self.assertEqual(b_meta['summaries'], ['q5', 'q14_1']) x_y_map = [('q1', ['@']), ('q2', ['@']), ('q2b', ['@']), ('q3', ['@']), ('q4', ['@']), ('q5', ['@']), (u'q5_1', ['@']), (u'q5_2', ['@']), @@ -169,11 +171,13 @@ def test_copy(self): batch2 = batch1.clone('test_copy') batch3 = batch1.clone('test_copy2', as_addition=True) attributes = ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', - 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', - 'transposed_arrays', 'extended_yks_global', 'extended_yks_per_x', + 'x_filter_map', 'y_on_y', 'forced_names', + 'transposed', 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language'] + 'sample_size', 'language', '_variables', 'y_on_y_filter', + 'leveled', 'skip_items', 'total', 'unwgt_counts', + '_section_starts'] for a in attributes: value = batch1.__dict__[a] value2 = batch2.__dict__[a] @@ -212,20 +216,42 @@ def test_set_sigtest(self): batch.set_sigtests(.05) self.assertEqual(_get_meta(batch)['sigproperties']['siglevels'], [0.05]) - def test_make_summaries_transpose_arrays(self): - batch, ds = _get_batch('test') + def test_level(self): + batch, ds = _get_batch('test', full=True) + batch.add_downbreak(['q5', 'q6']) + batch.level(['q5', 'q6'], {'q5': ['@', 'gender']}) + b_meta = _get_meta(batch) + leveled = { + 'q5': ['@', 'gender'], + 'q6': ['@', 'gender', 'q2']} + self.assertEqual(b_meta['leveled'], leveled) + + def test_transpose(self): + batch, ds = _get_batch('test', full=True) + batch.add_downbreak(['q5', 'q6']) + batch.transpose(['q5']) + batch.exclusive_arrays(['q5', 'q6']) b_meta = _get_meta(batch) - batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) - batch.make_summaries(None) - self.assertEqual(b_meta['summaries'], []) - batch.transpose_arrays(['q5', 'q6'], False) - batch.transpose_arrays(['q14_2', 'q14_3'], True) - self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) - t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} - self.assertEqual(b_meta['transposed_arrays'], t_a) - batch.make_summaries('q5') - self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) - self.assertRaises(ValueError, batch.make_summaries, 'q7') + x_y_map = [ + ('q5', ['@']), + ('@', ['q5']), + ('q6', ['@'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) + + # def test_make_summaries_transpose_arrays(self): + # batch, ds = _get_batch('test') + # b_meta = _get_meta(batch) + # batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + # batch.make_summaries(None) + # self.assertEqual(b_meta['summaries'], []) + # batch.transpose_arrays(['q5', 'q6'], False) + # batch.transpose_arrays(['q14_2', 'q14_3'], True) + # self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) + # t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} + # self.assertEqual(b_meta['transposed_arrays'], t_a) + # batch.make_summaries('q5') + # self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) + # self.assertRaises(ValueError, batch.make_summaries, 'q7') def test_extend_y(self): batch1, ds = _get_batch('test1', full=True) @@ -235,26 +261,25 @@ def test_extend_y(self): self.assertRaises(ValueError, batch1.extend_y, 'q2b', 'q5') batch1.extend_y('q2b') x_y_map = [('q1', ['@', 'gender', 'q2', 'q2b']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q2b']), - (u'q6_2', ['@', 'gender', 'q2', 'q2b']), - (u'q6_3', ['@', 'gender', 'q2', 'q2b']), - ('age', ['@', 'gender', 'q2', 'q2b'])] + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q2b']), + (u'q6_2', ['@', 'gender', 'q2', 'q2b']), + (u'q6_3', ['@', 'gender', 'q2', 'q2b']), + ('age', ['@', 'gender', 'q2', 'q2b'])] self.assertEqual(b_meta1['x_y_map'], x_y_map) - self.assertEqual(b_meta1['extended_yks_global'], ['q2b']) batch2.extend_y('q2b', 'q2') batch2.extend_y('q3', 'q6') extended_yks_per_x = {u'q6_3': ['q3'], 'q2': ['q2b'], u'q6_1': ['q3'], - u'q6_2': ['q3'], 'q6': ['q3']} + u'q6_2': ['q3']} self.assertEqual(b_meta2['extended_yks_per_x'], extended_yks_per_x) x_y_map = [('q1', ['@', 'gender', 'q2']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q3']), - (u'q6_2', ['@', 'gender', 'q2', 'q3']), - (u'q6_3', ['@', 'gender', 'q2', 'q3']), - ('age', ['@', 'gender', 'q2'])] + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q3']), + (u'q6_2', ['@', 'gender', 'q2', 'q3']), + (u'q6_3', ['@', 'gender', 'q2', 'q3']), + ('age', ['@', 'gender', 'q2'])] self.assertEqual(b_meta2['x_y_map'], x_y_map) def test_replace_y(self): @@ -264,8 +289,7 @@ def test_replace_y(self): batch.replace_y('q2b', 'q6') exclusive_yks_per_x = {u'q6_3': ['@', 'q2b'], u'q6_1': ['@', 'q2b'], - u'q6_2': ['@', 'q2b'], - 'q6': ['@', 'q2b']} + u'q6_2': ['@', 'q2b']} self.assertEqual(b_meta['exclusive_yks_per_x'], exclusive_yks_per_x) x_y_map = [('q1', ['@', 'gender', 'q2']), ('q2', ['@', 'gender', 'q2']), From d98ab1361e445ba33254be2553f9b46b9e1430e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 15 Mar 2019 14:04:13 +0100 Subject: [PATCH 670/733] transposed key in stack --- quantipy/core/stack.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index 5b14689a4..773d6515b 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -2395,10 +2395,12 @@ def _check_and_update_calc(calc_expression, text_key): raise ValueError(msg.format(v)) else: check_on.append(v) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] - + for b in _batches: + batch = meta['sets']['batches'][b] + transposed = batch.get('transposed_arrays', batch['transposed']) + if v in transposed: + on_vars += [('@', v)] + break if not only_recode: all_batches = copy.deepcopy(meta['sets']['batches']) for n, b in all_batches.items(): @@ -2646,9 +2648,12 @@ def _add_factors(v, meta, values, args): check_on = list(set(check_on + [items[0]])) else: check_on = list(set(check_on + [v])) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] + for b in _batches: + batch = meta['sets']['batches'][b] + transposed = batch.get('transposed_arrays', batch['transposed']) + if v in transposed: + on_vars += [('@', v)] + break ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) ds.from_stack(self, dk) From 4fe36db4eff2d9c7d2604ee299d07d947e3ae92d Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 18 Mar 2019 12:56:02 +0100 Subject: [PATCH 671/733] Level class to aggregate summarized arrays --- quantipy/core/batch.py | 2 +- quantipy/core/dataset.py | 42 +++++++++++- quantipy/core/quantify/engine.py | 74 ++++++++++++++++++++++ quantipy/core/view_generators/view_maps.py | 12 ++++ 4 files changed, 128 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 288cab552..e63a6777b 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -606,7 +606,7 @@ def codes_in_data(self, name): else: data_codes = pd.get_dummies(data).columns.tolist() return data_codes - + def hide_empty(self, xks=True, summaries=True): """ Drop empty variables and hide array items from summaries. diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index f382a9d4d..fc632b5be 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4478,6 +4478,46 @@ def interlock(self, name, label, variables, val_text_sep = '/'): self.drop(var) return None + @verify(variables={'name': 'masks'}) + def level(self, name): + """ + """ + self.copy(name, 'level') + if self._dimensions_comp: + temp = self._dims_free_arr_name(name) + lvlname = self._dims_compat_arr_name('{}_level'.format(temp)) + else: + lvlname = '{}_level'.format(name) + items = self.items(name) + sources = enumerate(self.sources(lvlname), 1) + codes = self.codes(lvlname) + max_code = len(codes) + replace_codes = {} + mapped_codes = {c: [] for c in self.codes(name)} + + for no, source in sources: + offset = (no-1) * max_code + new_codes = frange('{}-{}'.format((offset + 1), (offset + max_code))) + replace_codes[source] = dict(zip(codes, new_codes)) + + for source, codes in replace_codes.items(): + self[source].replace(codes, inplace=True) + self[source].replace(np.NaN, '', inplace=True) + for org, new in codes.items(): + mapped_codes[org].append(new) + + code_range = frange('1-{}'.format(max_code * len(items))) + labels = self.value_texts(name) * len(items) + cats = zip(code_range, labels) + new_sources = self.sources(lvlname) + self.unbind(lvlname) + self.add_meta(lvlname, 'delimited set', self.text(name), cats) + self[lvlname] = self[new_sources].astype('str').apply(lambda x: ';'.join(x).replace('.0', ''), axis=1) + self.drop(new_sources) + self._meta['columns'][lvlname]['properties']['level'] = {'source': name, + 'level_codes': mapped_codes} + return None + @verify(text_keys='text_key') def derive(self, name, qtype, label, cond_map, text_key=None): """ @@ -6213,7 +6253,7 @@ def get_property(self, name, prop_name, text_key=None): if not text_key: text_key = self.text_key valid_props = ['base_text', 'created', 'recoded_net', 'recoded_stat', 'recoded_filter', '_no_valid_items', '_no_valid_values', - 'simple_org_expr'] + 'simple_org_expr', 'level'] if prop_name not in valid_props: raise ValueError("'prop_name' must be one of {}".format(valid_props)) has_props = False diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index cfd07eb0a..422883b84 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -64,6 +64,7 @@ def __init__(self, link, weight=None, base_all=False, ignore_flags=False): self.type = self._get_type() if self.type == 'nested': self.nest_def = Nest(self.y, self.d(), self.meta()).nest() + self.levelled = self.ds.get_property(self.x, 'level') self._squeezed = False self.idx_map = None self.xdef = self.ydef = None @@ -250,6 +251,8 @@ def swap(self, var, axis='x', update_axis_def=True, inplace=True): swapped.x, swapped.y = x, y swapped.f, swapped.w = f, w swapped.type = swapped._get_type() + if swapped.type == 'nested': + swapped.nest_def = Nest(swapped.y, swapped.d(), swapped.meta()).nest() swapped._get_matrix() if not update_axis_def and array_swap: swapped.x = org_name @@ -2347,3 +2350,74 @@ def _interlock_texts(self): interlocked_valtexts = list(product(*all_valtexts)) interlocked_qtexts = list(product(*all_qtexts)) return interlocked_qtexts, interlocked_valtexts + +class Level(object): + """ + """ + def __init__(self, quantity): + """ + """ + self.quantity = quantity + self.dataset = self.quantity.ds + self._lvlspec = self.dataset.get_property(self.quantity.x, 'level') + self.array = self._lvlspec['source'] + self.level_codes = self._lvlspec['level_codes'] + self._auxdf = self.quantity.count(margin=False).result.reset_index() + self._collapse_codes() + self.lvldf = None + + def _reindex(self, like='freq'): + ds = self.dataset + arr = self.array + itemres = self.quantity.swap(ds.sources(arr)[0], axis='x', inplace=False) + if like == 'freq': + itemres.count(margin=False, axis=None, as_df=True) + self.lvldf = self.lvldf.reindex(ds.codes(arr)) + elif like == 'base': + itemres.count(margin=False, axis='x', as_df=True) + x = [self.quantity.x] + vals = itemres.result.index.get_level_values(1).tolist() + idx = pd.MultiIndex.from_product([x, vals], + names=['Question', 'Values']) + self.lvldf.index = idx + None + + def _collapse_codes(self): + df = self._auxdf + for org, lvls in self.level_codes.items(): + for lvl in lvls: + df['Values'] = df['Values'].replace( + lvl, int(org), inplace=False) + return None + + def count(self): + """ + """ + df = self._auxdf.set_index(['Question', 'Values']) + self.lvldf = df.sum(level=1, axis=0) + self._reindex() + return None + + def base(self): + """ + """ + df = self._auxdf.set_index(['Question', 'Values']) + self.lvldf = df.sum(level=0, axis=0) + self._reindex(like='base') + return None + + def percent(self): + """ + """ + self.count() + c = self.lvldf + self.base() + b = self.lvldf + pcts = c.values / b.values * 100 + self.lvldf = pd.DataFrame(pcts, index=c.index, columns=c.columns) + return None + + def as_view(self): + """ + """ + pass \ No newline at end of file diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index fabd4ebd8..a1480336f 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -23,6 +23,7 @@ import quantipy as qp from quantipy.core.cache import Cache +from quantipy.core.quantify.engine import Level import time class QuantipyViews(ViewMapper): @@ -366,6 +367,17 @@ def frequency(self, link, name, kwargs): else: view.dataframe = q.result view._kwargs['exclude'] = q.miss_x + + if q.levelled and not logic and not calc: + levelled = Level(q) + if rel_to is not None: + levelled.percent() + elif axis == 'x': + levelled.base() + else: + levelled.count() + view.dataframe = levelled.lvldf + link[notation] = view def descriptives(self, link, name, kwargs): From 43a3ed29d2e94d432c4209bc4aaa10bb656132d3 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 20 Mar 2019 06:55:56 +0100 Subject: [PATCH 672/733] no reset of dataset meta if stack filters are needed --- quantipy/core/stack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index d65bc59b0..dbf5473b4 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -778,7 +778,8 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, continue else: dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) + dataset.from_components(self[dk].data, self[dk].meta, + reset=False) f_dataset = dataset.filter(filter_def, logic, inplace=False) self[dk][filter_def].data = f_dataset._data self[dk][filter_def].meta = f_dataset._meta From bb6a6917f7ca5a8c1e022fee5975d948f7bb9ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 22 Mar 2019 11:49:29 +0100 Subject: [PATCH 673/733] fix tests --- quantipy/core/dataset.py | 2 +- quantipy/core/quantify/engine.py | 5 +++- quantipy/core/view_generators/view_maps.py | 12 ++++----- tests/test_stack.py | 29 +++++++++++----------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e3318fc5d..b641e9856 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -316,7 +316,7 @@ def _clear_cache(self): def _get_type(self, var): if var in self._meta['masks'].keys(): return self._meta['masks'][var]['type'] - else: + if var in self._meta['columns'].keys(): return self._meta['columns'][var]['type'] def _get_subtype(self, name): diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index 422883b84..bc63bb205 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -64,7 +64,10 @@ def __init__(self, link, weight=None, base_all=False, ignore_flags=False): self.type = self._get_type() if self.type == 'nested': self.nest_def = Nest(self.y, self.d(), self.meta()).nest() - self.levelled = self.ds.get_property(self.x, 'level') + if not self.x == '@': + self.leveled = self.ds.get_property(self.x, 'level') + else: + self.leveled = False self._squeezed = False self.idx_map = None self.xdef = self.ydef = None diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index a1480336f..0a7b2dd07 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -368,15 +368,15 @@ def frequency(self, link, name, kwargs): view.dataframe = q.result view._kwargs['exclude'] = q.miss_x - if q.levelled and not logic and not calc: - levelled = Level(q) + if q.leveled and not logic and not calc: + leveled = Level(q) if rel_to is not None: - levelled.percent() + leveled.percent() elif axis == 'x': - levelled.base() + leveled.base() else: - levelled.count() - view.dataframe = levelled.lvldf + leveled.count() + view.dataframe = leveled.lvldf link[notation] = view diff --git a/tests/test_stack.py b/tests/test_stack.py index d461b6dac..1fd410858 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1115,11 +1115,10 @@ def test_stack_aggregate(self): b2.add_downbreak(['q1', 'q6']) b2.add_crossbreak(['gender', 'q2']) b2.set_weights('weight_b') - b2.transpose_arrays('q6', True) + b2.transpose('q6') b3.add_downbreak(['q1', 'q7']) b3.add_crossbreak(['q2b']) b3.add_y_on_y('y_on_y') - b3.make_summaries(None) b3.set_weights(['weight_a', 'weight_b']) stack = ds.populate(verbose=False) stack.aggregate(['cbase', 'counts', 'c%'], True, @@ -1131,19 +1130,21 @@ def test_stack_aggregate(self): 'x|f|:|y|weight_a|c%', 'x|f|:|y|weight_b|c%', 'x|f|:||weight_a|counts', 'x|f|:||weight_b|counts', 'x|f|x:||weight_a|cbase', 'x|f|x:||weight_b|cbase', 'x|f|x:|||cbase'] - cols = ['@', 'age', 'q1', 'q2b', 'q6', u'q6_1', u'q6_2', u'q6_3', u'q7_1', + cols = ['@', 'age', 'q1', 'q2b', 'q6', u'q6_1', u'q6_2', u'q6_3', u'q7', u'q7_1', u'q7_2', u'q7_3', u'q7_4', u'q7_5', u'q7_6'] - values = [['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 3.0, 6.0, 'NONE', 1.0, 6.0, 6.0, 6.0, 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE']] + values = [ + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0,2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 3.0, 6.0, 'NONE', 2.0, 6.0, 6.0, 6.0, 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE']] + describe = stack.describe('view', 'x').replace(numpy.NaN, 'NONE') self.assertEqual(describe.index.tolist(), index) self.assertEqual(describe.columns.tolist(), cols) From caf805ab3fbb08e7f2716eb1ddab2c0c5c65f776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 22 Mar 2019 12:21:13 +0100 Subject: [PATCH 674/733] change error msg in batch --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 12d7d09a5..32458dad5 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -653,7 +653,7 @@ def transpose_arrays(self, arrays, replace=False): Deprecated! Transposed summary tables are created for defined arrays. """ msg = ("Depricated! `transpose_arrays()` is not available anymore, " - "please use `exclusive_arrays()` to skip items.") + "please use `transpose()` instead.") raise NotImplementedError(msg) @modify(to_list=['array']) From 37d32052f9890fcb6496cb5ac9db94e9f933f246 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 27 Mar 2019 13:13:39 +0100 Subject: [PATCH 675/733] level process --- quantipy/core/batch.py | 32 ++++++++++++++++------ quantipy/core/dataset.py | 14 +++++----- quantipy/core/view_generators/view_maps.py | 30 ++++++++++---------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 32458dad5..3a4f59aff 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -180,6 +180,7 @@ def _update(self): """ self._map_x_to_y() self._map_x_to_filter() + self._split_level_arrays() self._map_y_on_y_filter() self._samplesize_from_batch_filter() attrs = self.__dict__ @@ -690,26 +691,26 @@ def transpose(self, name): @modify(to_list='array') @verify(variables={'array': 'masks'}) - def level(self, array, levels={}): + def level(self, array): """ - Level arrays by defined level variables. + Produce leveled (a flat view of all item reponses) array aggregations. Parameters ---------- array: str/ list of str Names of the arrays to add the levels to. - levels: list/ dict of lists - The levels which should be used for the specific arrays. + + Returns + ------- + None """ not_valid = [a for a in array if a not in self.xks] if not_valid: raise ValueError('{} not defined as xks.'.format(not_valid)) for a in array: - if not isinstance(levels, list): - l = levels.get(a) or self.yks - else: - l = levels - self.leveled[a] = l + self.leveled[a] = self.yks + if not '{}_level'.format(a) in self: + self._level(a) self._update() return None @@ -1083,6 +1084,19 @@ def _get_yks(x): self.x_y_map = mapping return None + def _split_level_arrays(self): + _x_y_map = [] + for x, y in self.x_y_map: + if x in self.leveled.keys(): + lvl_name = '{}_level'.format(x) + _x_y_map.append((x, ['@'])) + _x_y_map.append((lvl_name, y)) + self.x_filter_map[lvl_name] = self.x_filter_map[x] + else: + _x_y_map.append((x, y)) + self.x_y_map = _x_y_map + return None + def _map_x_to_filter(self): """ Combine all defined downbreaks with its beloning filter in a map. diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index b641e9856..e437cff93 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4538,7 +4538,7 @@ def interlock(self, name, label, variables, val_text_sep = '/'): return None @verify(variables={'name': 'masks'}) - def level(self, name): + def _level(self, name): """ """ self.copy(name, 'level') @@ -4571,10 +4571,11 @@ def level(self, name): new_sources = self.sources(lvlname) self.unbind(lvlname) self.add_meta(lvlname, 'delimited set', self.text(name), cats) - self[lvlname] = self[new_sources].astype('str').apply(lambda x: ';'.join(x).replace('.0', ''), axis=1) + self[lvlname] = self[new_sources].astype('str').apply( + lambda x: ';'.join(x).replace('.0', ''), axis=1) self.drop(new_sources) - self._meta['columns'][lvlname]['properties']['level'] = {'source': name, - 'level_codes': mapped_codes} + self._meta['columns'][lvlname]['properties']['level'] = { + 'source': name, 'level_codes': mapped_codes} return None @verify(text_keys='text_key') @@ -7148,19 +7149,18 @@ def populate(self, batches='all', verbose=True): ------- qp.Stack """ + dk = self.name meta = self._meta data = self._data stack = qp.Stack(name='aggregations', add_data={dk: (data, meta)}) batches = stack._check_batches(dk, batches) - for name in batches: - batch = self._meta['sets']['batches'][name] + batch = meta['sets']['batches'][name] xys = batch['x_y_map'] fs = batch['x_filter_map'] fy = batch['y_filter_map'] my = batch['yks'] - total_len = len(xys) + len(batch['y_on_y']) for idx, xy in enumerate(xys, start=1): x, y = xy diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index 0a7b2dd07..b8bb36433 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -330,7 +330,16 @@ def frequency(self, link, name, kwargs): if q.type == 'array' and not q.y == '@': pass else: - if logic is not None: + if q.leveled: + leveled = Level(q) + if rel_to is not None: + leveled.percent() + elif axis == 'x': + leveled.base() + else: + leveled.count() + view.dataframe = leveled.lvldf + elif logic is not None: try: q.group(groups=logic, axis=axis, expand=expand, complete=complete) except NotImplementedError, e: @@ -362,21 +371,12 @@ def frequency(self, link, name, kwargs): method_nota = 'f' notation = view.notation(method_nota, condition) view._notation = notation - if q.type == 'array': - view.dataframe = q.result.T if link.y == '@' else q.result - else: - view.dataframe = q.result - view._kwargs['exclude'] = q.miss_x - - if q.leveled and not logic and not calc: - leveled = Level(q) - if rel_to is not None: - leveled.percent() - elif axis == 'x': - leveled.base() + if not q.leveled: + if q.type == 'array': + view.dataframe = q.result.T if link.y == '@' else q.result else: - leveled.count() - view.dataframe = leveled.lvldf + view.dataframe = q.result + view._kwargs['exclude'] = q.miss_x link[notation] = view From 6b9390e8284770f515bcafb1c99c31120debe154 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 1 Apr 2019 11:40:07 +0200 Subject: [PATCH 676/733] new property for level array class feature --- quantipy/core/dataset.py | 4 +++- quantipy/core/quantify/engine.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e437cff93..8ec3c60b3 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4575,7 +4575,9 @@ def _level(self, name): lambda x: ';'.join(x).replace('.0', ''), axis=1) self.drop(new_sources) self._meta['columns'][lvlname]['properties']['level'] = { - 'source': name, 'level_codes': mapped_codes} + 'source': name, + 'level_codes': mapped_codes, + 'item_look': self.sources(name)[0]} return None @verify(text_keys='text_key') diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index bc63bb205..a2769f1e2 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -2365,17 +2365,18 @@ def __init__(self, quantity): self._lvlspec = self.dataset.get_property(self.quantity.x, 'level') self.array = self._lvlspec['source'] self.level_codes = self._lvlspec['level_codes'] + self.item_look = self._lvlspec['item_look'] self._auxdf = self.quantity.count(margin=False).result.reset_index() self._collapse_codes() self.lvldf = None def _reindex(self, like='freq'): ds = self.dataset - arr = self.array - itemres = self.quantity.swap(ds.sources(arr)[0], axis='x', inplace=False) + like_item = self.item_look + itemres = self.quantity.swap(like_item, axis='x', inplace=False) if like == 'freq': itemres.count(margin=False, axis=None, as_df=True) - self.lvldf = self.lvldf.reindex(ds.codes(arr)) + self.lvldf = self.lvldf.reindex(ds.codes(like_item)) elif like == 'base': itemres.count(margin=False, axis='x', as_df=True) x = [self.quantity.x] From fd00885725d7d1debe5b4209b7884de1b91a87b5 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 9 Apr 2019 09:24:50 +0200 Subject: [PATCH 677/733] fixed tests --- tests/test_batch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_batch.py b/tests/test_batch.py index 3ffb3b50d..3aafec021 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -219,10 +219,10 @@ def test_set_sigtest(self): def test_level(self): batch, ds = _get_batch('test', full=True) batch.add_downbreak(['q5', 'q6']) - batch.level(['q5', 'q6'], {'q5': ['@', 'gender']}) + batch.level(['q5', 'q6']) b_meta = _get_meta(batch) leveled = { - 'q5': ['@', 'gender'], + 'q5': ['@', 'gender', 'q2'], 'q6': ['@', 'gender', 'q2']} self.assertEqual(b_meta['leveled'], leveled) From 452647fc911a099040ba7ff41d4a1a4743a3bea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 18 Apr 2019 13:30:25 +0200 Subject: [PATCH 678/733] get back commit --- .pytest_cache/v/cache/nodeids | 3 +- quantipy/core/batch.py | 403 +++++++++++++-------- quantipy/core/dataset.py | 243 +++++++++++-- quantipy/core/quantify/engine.py | 78 ++++ quantipy/core/stack.py | 24 +- quantipy/core/tools/qp_decorators.py | 30 +- quantipy/core/view_generators/view_maps.py | 22 +- quantipy/sandbox/sandbox.py | 16 +- tests/test_batch.py | 106 +++--- tests/test_stack.py | 29 +- 10 files changed, 693 insertions(+), 261 deletions(-) diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index fc4d323c1..203477c9a 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -16,7 +16,7 @@ "tests/test_batch.py::TestBatch::test_extend_y", "tests/test_batch.py::TestBatch::test_from_batch", "tests/test_batch.py::TestBatch::test_hiding", - "tests/test_batch.py::TestBatch::test_make_summaries_transpose_arrays", + "tests/test_batch.py::TestBatch::test_level", "tests/test_batch.py::TestBatch::test_replace_y", "tests/test_batch.py::TestBatch::test_set_cell_items", "tests/test_batch.py::TestBatch::test_set_language", @@ -24,6 +24,7 @@ "tests/test_batch.py::TestBatch::test_set_weight", "tests/test_batch.py::TestBatch::test_slicing", "tests/test_batch.py::TestBatch::test_sorting", + "tests/test_batch.py::TestBatch::test_transpose", "tests/test_chain.py::TestChainConstructor::()::test_init", "tests/test_chain.py::TestChainConstructor::()::test_str", "tests/test_chain.py::TestChainConstructor::()::test_repr", diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 288cab552..b7b2ae8f9 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -131,7 +131,6 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self._variables = [] self._section_starts = {} self.total = True - self.extended_yks_global = None self.extended_yks_per_x = {} self.exclusive_yks_per_x = {} self.extended_filters_per_x = {} @@ -143,8 +142,10 @@ def __init__(self, dataset, name, ci=['c', 'p'], weights=None, tests=None): self.y_on_y_filter = {} self.y_filter_map = {} self.forced_names = {} - self.summaries = [] - self.transposed_arrays = {} + self.transposed = [] + self.leveled = {} + # self.summaries = [] + # self.transposed_arrays = {} self.skip_items = [] self.verbatims = [] # self.verbatim_names = [] @@ -179,13 +180,14 @@ def _update(self): """ self._map_x_to_y() self._map_x_to_filter() + self._split_level_arrays() self._map_y_on_y_filter() self._samplesize_from_batch_filter() attrs = self.__dict__ for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'name', 'skip_items', 'total', @@ -202,8 +204,8 @@ def _load_batch(self): bdefs = self._meta['sets']['batches'][self.name] for attr in ['xks', 'yks', '_variables', 'filter', 'filter_names', 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', 'sample_size', 'language', 'skip_items', 'total', 'unwgt_counts', @@ -423,7 +425,6 @@ def as_addition(self, batch_name): self._update() return None - def as_main(self, keep=True): """ Transform additional ``Batch`` definitions into regular (parent/main) ones. @@ -472,7 +473,6 @@ def add_variables(self, varlist): self._update() return None - @modify(to_list='dbrk') def add_downbreak(self, dbrk): """ @@ -492,28 +492,16 @@ def add_downbreak(self, dbrk): clean_xks = self._check_forced_names(dbrk) self.xks = self.unroll(clean_xks, both='all') self._update() - masks = [x for x in self.xks if x in self.masks()] - self.make_summaries(masks, [], _verbose=False) return None - @modify(to_list='xks') def add_x(self, xks): """ - Set the x (downbreak) variables of the Batch. - - Parameters - ---------- - xks: str, list of str, dict, list of dict - Names of variables that are used as downbreaks. Forced names for - Excel outputs can be given in a dict, for example: - xks = ['q1', {'q2': 'forced name for q2'}, 'q3', ....] + Deprecated! Set the x (downbreak) variables of the Batch. - Returns - ------- - None """ - w = "'add_x()' will be deprecated in a future version. Please use 'add_downbreak()' instead!" + w = ("'add_x()' will be deprecated in a future version. " + "Please use 'add_downbreak()' instead!") warnings.warn(w) self.add_downbreak(xks) @@ -540,25 +528,24 @@ def extend_x(self, ext_xks): for x in ext_xks: if isinstance(x, dict): for pos, var in x.items(): + if pos not in self: + raise KeyError('{} is not included.'.format(pos)) + elif self._is_array_item(pos): + msg = '{}: Cannot use an array item as position' + raise ValueError(msg.format(pos)) if not isinstance(var, list): var = [var] - var = self.unroll(var, both='all') for v in var: - if not self.var_exists(pos): - raise KeyError('{} is not included.'.format(pos)) - elif not v in self.xks: - self.xks.insert(self.xks.index(pos), v) - if self.is_array(v) and not v in self.summaries: - self.summaries.append(v) - elif not self.var_exists(x): - raise KeyError('{} is not included.'.format(x)) - elif x not in self.xks: - self.xks.extend(self.unroll(x, both='all')) - if self.is_array(x) and not x in self.summaries: - self.summaries.append(x) + if v in self.xks: + msg = '{} is already included as downbreak.' + raise ValueError(msg.format(v)) + self.xks.insert(self.xks.index(pos), v) + else: + if x not in self: + raise KeyError('{} is not included.'.format(x)) + self.xks.append(x) self._update() return None - def add_section(self, x_anchor, section): """ """ @@ -606,7 +593,7 @@ def codes_in_data(self, name): else: data_codes = pd.get_dummies(data).columns.tolist() return data_codes - + def hide_empty(self, xks=True, summaries=True): """ Drop empty variables and hide array items from summaries. @@ -636,7 +623,6 @@ def hide_empty(self, xks=True, summaries=True): self.hiding(x, e_items, axis='x', hide_values=False) if len(e_items) == len(sources): if x in self.xks: self.xks.remove(x) - if x in self.summaries: self.summaries.remove(x) removed_sum.append(x) if xks: for i in e_items: @@ -655,79 +641,77 @@ def hide_empty(self, xks=True, summaries=True): self._update() return None - @modify(to_list=['arrays']) - @verify(variables={'arrays': 'masks'}) def make_summaries(self, arrays, exclusive=False, _verbose=None): """ - Summary tables are created for defined arrays. + Deprecated! Summary tables are created for defined arrays. + """ + msg = ("Depricated! `make_summaries()` is not available anymore, please" + " use `exclusive_arrays()` to skip items.") + raise NotImplementedError(msg) + + def transpose_arrays(self, arrays, replace=False): + """ + Deprecated! Transposed summary tables are created for defined arrays. + """ + msg = ("Depricated! `transpose_arrays()` is not available anymore, " + "please use `transpose()` instead.") + raise NotImplementedError(msg) + + @modify(to_list=['array']) + @verify(variables={'array': 'masks'}) + def exclusive_arrays(self, array): + """ + For defined arrays only summary tables are produced. Items get ignored. + """ + not_valid = [a for a in array if a not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + self.skip_items = array + self._update() + return None + + @modify(to_list='name') + @verify(variables={'name': 'both'}) + def transpose(self, name): + """ + Create transposed aggregations for the requested variables. Parameters ---------- - arrays: str/ list of str - List of arrays for which summary tables are created. Summary tables - can only be created for arrays that are included in ``self.xks``. - exclusive: bool/ list, default False - If True only summaries are created and items skipped. ``exclusive`` - parameter can be provided for a selection of arrays. Example:: - >>> b.make_summaries(['array1', 'array2'], exclusive = ['array2']) - Returns - ------- - None - """ - if _verbose is None: _verbose = self._verbose_infos - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - self.summaries = arrays - if exclusive: - if isinstance(exclusive, bool): - self.skip_items = arrays - else: - self.skip_items = [a for a in exclusive if a in arrays] - else: - self.skip_items = [] - if arrays: - msg = 'Array summaries setup: Creating {}.'.format(arrays) - else: - msg = 'Array summaries setup: Creating no summaries!' - if _verbose: - print msg - for t_array in self.transposed_arrays.keys(): - if not t_array in arrays: - self.transposed_arrays.pop(t_array) + name: str/ list of str + Name of variable(s) for which transposed aggregations will be + created. + """ + not_valid = [n for n in name if n not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + self.transposed = name self._update() return None - @modify(to_list='arrays') - @verify(variables={'arrays': 'masks'}) - def transpose_arrays(self, arrays, replace=False): + @modify(to_list='array') + @verify(variables={'array': 'masks'}) + def level(self, array): """ - Transposed summary tables are created for defined arrays. + Produce leveled (a flat view of all item reponses) array aggregations. Parameters ---------- - arrays: str/ list of str - List of arrays for which transposed summary tables are created. - Transposed summary tables can only be created for arrays that are - included in ``self.xks``. - replace: bool, default True - If True only the transposed table is created, if False transposed - and normal summary tables are created. + array: str/ list of str + Names of the arrays to add the levels to. Returns ------- None """ - if any(a not in self.xks for a in arrays): - msg = '{} not defined as xks.'.format([a for a in arrays if not a in self.xks]) - raise ValueError(msg) - if any(a not in self.summaries for a in arrays): - ar = list(set(self.summaries + arrays)) - a = [v for v in self.xks if v in ar] - self.make_summaries(a, []) - for array in arrays: - self.transposed_arrays[array] = replace - self._update() + not_valid = [a for a in array if a not in self.xks] + if not_valid: + raise ValueError('{} not defined as xks.'.format(not_valid)) + for a in array: + self.leveled[a] = self.yks + if not '{}_level'.format(a) in self: + self._level(a) + self._update() return None def add_total(self, total=True): @@ -743,7 +727,7 @@ def add_total(self, total=True): return None @modify(to_list='xbrk') - @verify(variables={'xbrk': 'both'}, categorical='xbrk') + @verify(variables={'xbrk': 'both_nested'}, categorical='xbrk') def add_crossbreak(self, xbrk): """ Set the y (crossbreak/banner) variables of the Batch. @@ -767,41 +751,16 @@ def add_crossbreak(self, xbrk): return None @modify(to_list='yks') - @verify(variables={'yks': 'both'}, categorical='yks') + @verify(variables={'yks': 'both_nested'}, categorical='yks') def add_y(self, yks): """ - Set the y (crossbreak/banner) variables of the Batch. - - Parameters - ---------- - yks: str, list of str - Variables that are added as crossbreaks. '@'/ total is added - automatically. - - Returns - ------- - None + Deprecated! Set the y (crossbreak/banner) variables of the Batch. """ - w = "'add_y()' will be deprecated in a future version. Please use 'add_crossbreak()' instead!" + w = ("'add_y()' will be deprecated in a future version. Please use" + " 'add_crossbreak()' instead!") warnings.warn(w) self.add_crossbreak(yks) - def add_x_per_y(self, x_on_y_map): - """ - Add individual combinations of x and y variables to the Batch. - - !!! Currently not implemented !!! - """ - raise NotImplementedError('NOT YET SUPPPORTED') - if not isinstance(x_on_y_map, list): x_on_y_maps = [x_on_y_map] - if not isinstance(x_on_y_maps[0], dict): - raise TypeError('Must pass a (list of) dicts!') - for x_on_y_map in x_on_y_maps: - for x, y in x_on_y_map.items(): - if not isinstance(y, list): y = [y] - if isinstance(x, tuple): x = {x[0]: x[1]} - return None - def add_filter(self, filter_name, filter_logic=None, overwrite=False): """ Apply a (global) filter to all the variables found in the Batch. @@ -936,7 +895,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): return None @modify(to_list=['ext_yks', 'on']) - @verify(variables={'ext_yks': 'columns'}) + @verify(variables={'ext_yks': 'both_nested'}) def extend_y(self, ext_yks, on=None): """ Add y (crossbreak/banner) variables to specific x (downbreak) variables. @@ -959,27 +918,22 @@ def extend_y(self, ext_yks, on=None): ------- None """ - ext_yks = [e for e in ext_yks if not e in self.yks] if not on: - self.yks.extend(ext_yks) - if not self.extended_yks_global: - self.extended_yks_global = ext_yks - else: - self.extended_yks_global.extend(ext_yks) + self.add_crossbreak(self.yks + ext_yks) else: - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + not_valid = [o for o in on if not o in self.xks] + if not_valid: + msg = '{} not defined as xks.'.format(not_valid) raise ValueError(msg) - on = self.unroll(on, both='all') + on = self.unroll(on) for x in on: - x_ext_yks = [e for e in ext_yks - if not e in self.extended_yks_per_x.get(x, [])] - self.extended_yks_per_x.update({x: x_ext_yks}) - self._update() + x_ext = self.unroll(self.extended_yks_per_x.get(x, []) + ext_yks) + self.extended_yks_per_x.update({x: x_ext}) + self._update() return None @modify(to_list=['new_yks', 'on']) - @verify(variables={'new_yks': 'both', 'on': 'both'}) + @verify(variables={'new_yks': 'both_nested', 'on': 'both'}) def replace_y(self, new_yks, on): """ Replace y (crossbreak/banner) variables on specific x (downbreak) variables. @@ -996,11 +950,13 @@ def replace_y(self, new_yks, on): ------- None """ - if any(o not in self.xks for o in on): - msg = '{} not defined as xks.'.format([o for o in on if not o in self.xks]) + not_valid = [o for o in on if not o in self.xks] + if not_valid: + msg = '{} not defined as xks.'.format(not_valid) raise ValueError(msg) - on = self.unroll(on, both='all') - if not '@' in new_yks: new_yks = ['@'] + new_yks + on = self.unroll(on) + if not '@' in new_yks and self.total: + new_yks = ['@'] + new_yks for x in on: self.exclusive_yks_per_x.update({x: new_yks}) self._update() @@ -1108,8 +1064,7 @@ def _get_yks(x): mapping = [] for x in self.xks: if self.is_array(x): - if x in self.summaries and not self.transposed_arrays.get(x): - mapping.append((x, ['@'])) + mapping.append((x, self.leveled.get(x, ['@']))) if not x in self.skip_items: try: hiding = self.meta_edits[x]['rules']['x']['dropx']['values'] @@ -1120,15 +1075,28 @@ def _get_yks(x): continue elif x2 in self.xks: mapping.append((x2, _get_yks(x2))) - if x in self.transposed_arrays: - mapping.append(('@', [x])) elif self._is_array_item(x) and self._maskname_from_item(x) in self.xks: continue else: mapping.append((x, _get_yks(x))) + if x in self.transposed: + mapping.append(('@', [x])) self.x_y_map = mapping return None + def _split_level_arrays(self): + _x_y_map = [] + for x, y in self.x_y_map: + if x in self.leveled.keys(): + lvl_name = '{}_level'.format(x) + _x_y_map.append((x, ['@'])) + _x_y_map.append((lvl_name, y)) + self.x_filter_map[lvl_name] = self.x_filter_map[x] + else: + _x_y_map.append((x, y)) + self.x_y_map = _x_y_map + return None + def _map_x_to_filter(self): """ Combine all defined downbreaks with its beloning filter in a map. @@ -1226,3 +1194,140 @@ def _samplesize_from_batch_filter(self): idx = self._data.index self.sample_size = len(idx) return None + + @modify(to_list=["mode", "misc"]) + def to_dataset(self, mode=None, from_set=None, additions="sort_within", + manifest_edits="keep", integrate_rc=(["_rc", "_rb"], True), + misc=["RecordNo", "caseid", "identity"]): + """ + Create a qp.DataSet instance out of the batch settings. + + Parameters + ---------- + mode: list of str {'x', 'y', 'v', 'oe', 'w', 'f'} + Variables to keep. + from_set: str or list of str, default None + Set name or a list of variables to sort against. + additions: str {'sort_within, sort_between', False} + Add variables from additional batches. + manifest_edits: str {'keep', 'apply', False} + Keep meta from edits or apply rules. + """ + batches = self._meta['sets']['batches'] + adds = batches[self.name]['additions'] + + # prepare variable list + if not mode: + mode = ['x', 'y', 'v', 'oe', 'w', 'f'] + vlist = self._get_vlist(batches[self.name], mode) + if additions == "sort_between": + for add in adds: + vlist += self._get_vlist(batches[add], mode) + vlist = self.align_order(vlist, from_set, integrate_rc, fix=misc) + if additions == "sort_within": + for add in adds: + add_list = self._get_vlist(batches[add], mode) + add_list = self.align_order(add_list, from_set, integrate_rc, + fix=misc) + vlist += add_list + vlist = self.de_duplicate(vlist) + vlist = self.roll_up(vlist) + + # handle filters + merge_f = False + f = self.filter + if adds: + filters = [self.filter] + [batches[add]['filter'] for add in adds] + filters = [fi for fi in filters if fi] + if len(filters) == 1: + f = filters[0] + elif not self.compare_filter(filters[0], filters[1:]): + f = "merge_filter" + merge_f = filters + else: + f = filters[0] + + # create ds + ds = qp.DataSet(self.name, self._dimensions_comp) + ds.from_components(self._data.copy(), org_copy.deepcopy(self._meta), + True, self.language) + + if merge_f: + ds.merge_filter(f, filters) + if not manifest_edits: + vlist.append(f) + if f and manifest_edits: + ds.filter(self.name, {f: 0}, True) + if merge_f: + ds.drop(f) + + ds.create_set(str(self.name), included=vlist, overwrite=True) + ds.subset(from_set=self.name, inplace=True) + ds.order(vlist) + + # manifest edits + if manifest_edits in ['apply', 'keep']: + b_meta = batches[self.name]['meta_edits'] + for v in ds.variables(): + if ds.is_array(v) and b_meta.get(v): + ds._meta['masks'][v] = b_meta[v] + try: + ds._meta['lib']['values'][v] = b_meta['lib'][v] + except: + pass + elif b_meta.get(v): + ds._meta['columns'][v] = b_meta[v] + if manifest_edits == "apply" and not ds._is_array_item(v): + for axis in ['x', 'y']: + if all(rule in ds._get_rules(v, axis) + for rule in ['dropx', 'slicex']): + drops = ds._get_rules(v, axis)['dropx']['values'] + slicer = ds._get_rules(v, axis)['slicex']['values'] + elif 'dropx' in ds._get_rules(v, axis): + drops = ds._get_rules(v, axis)['dropx']['values'] + slicer = ds.codes(v) + elif 'slicex' in ds._get_rules(v, axis): + drops = [] + slicer = ds._get_rules(v, axis)['slicex']['values'] + else: + drops = slicer = [] + if drops or slicer: + if not all(isinstance(c, int) for c in drops): + item_no = [ds.item_no(d) for d in drops] + ds.remove_items(v, item_no) + else: + codes = ds.codes(v) + n_codes = [c for c in slicer if not c in drops] + if not len(n_codes) == len(codes): + remove = [c for c in codes + if not c in n_codes] + ds.remove_values(v, remove) + ds.reorder_values(v, n_codes) + if ds.is_array(v): + ds._meta['masks'][v].pop('rules') + else: + ds._meta['columns'][v].pop('rules') + return ds + + def _get_vlist(self, batch, mode): + match = { + "x": "xks", + "y": "yks", + "v": "_variables", + "w": "weights", + "f": "filter", + "oe": "verbatims" + } + vlist = [] + for key in mode: + var = batch[match[key]] + if key == "oe": + oes = [] + for oe in var[:]: + oes += oe["break_by"] + oe["columns"] + [oe["filter"]] + var = oes + if not isinstance(var, list): var = [var] + for v in var: + if v and v in self and v not in vlist: + vlist.append(v) + return vlist diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9971c4665..8ff3269ca 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -316,7 +316,7 @@ def _clear_cache(self): def _get_type(self, var): if var in self._meta['masks'].keys(): return self._meta['masks'][var]['type'] - else: + if var in self._meta['columns'].keys(): return self._meta['columns'][var]['type'] def _get_subtype(self, name): @@ -1817,6 +1817,15 @@ def data(self): # Helpers # ------------------------------------------------------------------------ + @staticmethod + def de_duplicate(seq): + unique = [] + for x in seq: + if x in unique: + continue + unique.append(x) + return unique + @staticmethod def _is_all_ints(s): try: @@ -2905,8 +2914,23 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', # ------------------------------------------------------------------------ # lists/ sets of variables/ data file items # ------------------------------------------------------------------------ - @modify(to_list=['varlist']) - @verify(variables={'varlist': 'both'}) + + def _array_and_item_list(self, v, keep): + new_list = [] + if not self.is_array(v): + # columns + if keep in ['both', 'items']: + new_list.append(v) + else: + # masks + if keep in ['both', 'mask']: + new_list.append(v) + if keep in ['both', 'items']: + new_list.extend(self.sources(v)) + return new_list + + @modify(to_list=['varlist', 'ignore_arrays']) + @verify(variables={'varlist': 'both_nested', 'ignore_arrays': 'masks'}) def roll_up(self, varlist, ignore_arrays=None): """ Replace any array items with their parent mask variable definition name. @@ -2918,36 +2942,50 @@ def roll_up(self, varlist, ignore_arrays=None): ignore_arrays : (list of) str A list of array mask names that should not be rolled up if their items are found inside ``varlist``. + + Note + ---- + varlist can also contain nesting `var1 > var2`. The variables which are + included in the nesting can also be controlled by keep and both, even + if the variables are also included as a "normal" variable. + Returns ------- rolled_up : list The modified ``varlist``. """ - if ignore_arrays: - if not isinstance(ignore_arrays, list): - ignore_arrays = [ignore_arrays] - else: - ignore_arrays = [] - arrays_defs = {arr: self.sources(arr) for arr in self.masks() - if not arr in ignore_arrays} - item_map = {} - for k, v in arrays_defs.items(): - for item in v: - item_map[item] = k + def _var_to_keep(var, ignore): + if self.is_array(var): + to_keep = 'mask' + else: + to_keep = 'items' + if self._is_array_item(var): + parent = self._maskname_from_item(var) + if parent not in ignore_arrays: + var = parent + to_keep = 'mask' + return var, to_keep + rolled_up = [] - for v in varlist: - if not self.is_array(v): - if v in item_map: - if not item_map[v] in rolled_up: - rolled_up.append(item_map[v]) - else: - rolled_up.append(v) + for var in varlist: + if ' > ' in var: + nested = var.replace(' ', '').split('>') + n_list = [] + for n in nested: + n, to_keep = _var_to_keep(n, ignore_arrays) + n_list.append(self._array_and_item_list(n, to_keep)) + for ru in [' > '.join(list(un)) for un in product(*n_list)]: + if ru not in rolled_up: + rolled_up.append(ru) else: - rolled_up.append(v) + var, to_keep = _var_to_keep(var, ignore_arrays) + for ru in self._array_and_item_list(var, to_keep): + if ru not in rolled_up: + rolled_up.append(ru) return rolled_up @modify(to_list=['varlist', 'keep', 'both']) - @verify(variables={'varlist': 'both', 'keep': 'masks'}) + @verify(variables={'varlist': 'both_nested', 'keep': 'masks'}) def unroll(self, varlist, keep=None, both=None): """ Replace mask with their items, optionally excluding/keeping certain ones. @@ -2962,24 +3000,54 @@ def unroll(self, varlist, keep=None, both=None): The names of masks that will be included both as themselves and as collections of their items. + Note + ---- + varlist can also contain nesting `var1 > var2`. The variables which are + included in the nesting can also be controlled by keep and both, even + if the variables are also included as a "normal" variable. + + Example:: + >>> ds.unroll(varlist = ['q1', 'q1 > gender'], both='all') + ['q1', + 'q1_1', + 'q1_2', + 'q1 > gender', + 'q1_1 > gender', + 'q1_2 > gender'] + Returns ------- unrolled : list The modified ``varlist``. """ if both and both[0] == 'all': - both = [mask for mask in varlist if mask in self._meta['masks']] + both = self.masks() unrolled = [] for var in varlist: - if not self.is_array(var): - unrolled.append(var) + if ' > ' in var: + nested = var.replace(' ', '').split('>') + n_list = [] + for n in nested: + if n in keep: + to_keep = 'mask' + elif n in both: + to_keep = 'both' + else: + to_keep = 'items' + n_list.append(self._array_and_item_list(n, to_keep)) + for ur in [' > '.join(list(un)) for un in product(*n_list)]: + if ur not in unrolled: + unrolled.append(ur) else: - if not var in keep: - if var in both: - unrolled.append(var) - unrolled.extend(self.sources(var)) + if var in keep: + to_keep = 'mask' + elif var in both: + to_keep = 'both' else: - unrolled.append(var) + to_keep = 'items' + for ur in self._array_and_item_list(var, to_keep): + if ur not in unrolled: + unrolled.append(ur) return unrolled def _apply_order(self, variables): @@ -3061,6 +3129,57 @@ def _map_to_origins(self): if not v in sort_them: grouped.append(v) return grouped + @modify(to_list=['vlist', 'fix']) + def align_order(self, vlist, align_against=None, + integrate_rc=(["_rc", "_rb"], True), fix=[]): + """ + Align list to existing order. + + Parameters + ---------- + vlist: list of str + The list which should be reordered. + align_against: str or list of str, default None + The list of variables to align against. If a string is provided, + the depending set list is taken. If None, "data file" set is taken. + integrate_rc: tuple (list, bool) + The provided list are the suffixes for recodes, the bool decides + whether parent variables should be replaced by their recodes if + the parent variable is not in vlist. + fix: list of str + Variables which are fixed at the beginning of the reordered list. + """ + # get list to align against + if not align_against: + align_against = self._variables_from_set("data file") + elif isinstance(align_against, basestring): + align_against = self._variables_from_set(align_against) + + # recode suffixes and replace parent + if not integrate_rc: + integrate_rc = ([], False) + rec_suf, repl_parent = integrate_rc + + # create aligned order + new_vlist = fix[:] + for v in align_against: + recodes = ["{}{}".format(v, suf) for suf in rec_suf] + if v in vlist: + if v not in new_vlist: + new_vlist.append(v) + for rec in recodes: + if rec in vlist and rec not in new_vlist: + new_vlist.append(rec) + elif repl_parent: + for rec in recodes: + if rec in vlist and rec not in new_vlist: + new_vlist.append(rec) + + # add missing vars + miss = [v for v in vlist if v not in new_vlist] + new_vlist += miss + return new_vlist + @modify(to_list='reposition') def order(self, new_order=None, reposition=None, regroup=False): """ @@ -3380,6 +3499,17 @@ def manifest_filter(self, name): raise KeyError('{} is no valid filter-variable.'.format(name)) return self.take({name: 0}) + @modify(to_list="filters") + def merge_filter(self, name, filters): + if not all(f in self.filters() for f in filters): + raise KeyError("Not all included names are valid filters.") + logic = { + 'label': 'merged filter logics', + 'logic': union([{f: 0} for f in filters]) + } + self.add_filter_var(name, logic, True) + return None + @modify(to_list=['name2']) @verify(variables={'name1': 'both', 'name2': 'both'}) def compare_filter(self, name1, name2): @@ -4479,6 +4609,49 @@ def interlock(self, name, label, variables, val_text_sep = '/'): self.drop(var) return None + @verify(variables={'name': 'masks'}) + def _level(self, name): + """ + """ + self.copy(name, 'level') + if self._dimensions_comp: + temp = self._dims_free_arr_name(name) + lvlname = self._dims_compat_arr_name('{}_level'.format(temp)) + else: + lvlname = '{}_level'.format(name) + items = self.items(name) + sources = enumerate(self.sources(lvlname), 1) + codes = self.codes(lvlname) + max_code = len(codes) + replace_codes = {} + mapped_codes = {c: [] for c in self.codes(name)} + + for no, source in sources: + offset = (no-1) * max_code + new_codes = frange('{}-{}'.format((offset + 1), (offset + max_code))) + replace_codes[source] = dict(zip(codes, new_codes)) + + for source, codes in replace_codes.items(): + self[source].replace(codes, inplace=True) + self[source].replace(np.NaN, '', inplace=True) + for org, new in codes.items(): + mapped_codes[org].append(new) + + code_range = frange('1-{}'.format(max_code * len(items))) + labels = self.value_texts(name) * len(items) + cats = zip(code_range, labels) + new_sources = self.sources(lvlname) + self.unbind(lvlname) + self.add_meta(lvlname, 'delimited set', self.text(name), cats) + self[lvlname] = self[new_sources].astype('str').apply( + lambda x: ';'.join(x).replace('.0', ''), axis=1) + self.drop(new_sources) + self._meta['columns'][lvlname]['properties']['level'] = { + 'source': name, + 'level_codes': mapped_codes, + 'item_look': self.sources(name)[0]} + return None + @verify(text_keys='text_key') def derive(self, name, qtype, label, cond_map, text_key=None): """ @@ -6230,7 +6403,7 @@ def get_property(self, name, prop_name, text_key=None): if not text_key: text_key = self.text_key valid_props = ['base_text', 'created', 'recoded_net', 'recoded_stat', 'recoded_filter', '_no_valid_items', '_no_valid_values', - 'simple_org_expr'] + 'simple_org_expr', 'level'] if prop_name not in valid_props: raise ValueError("'prop_name' must be one of {}".format(valid_props)) has_props = False @@ -6698,7 +6871,6 @@ def set_missings(self, var, missing_map='default', hide_on_y=True, else: self._meta['columns'][v]['missings'] = v_m_map if hide_on_y: - print missing_map self.hiding(var, missing_map, 'y', True) return None @@ -7068,19 +7240,18 @@ def populate(self, batches='all', verbose=True): ------- qp.Stack """ + dk = self.name meta = self._meta data = self._data stack = qp.Stack(name='aggregations', add_data={dk: (data, meta)}) batches = stack._check_batches(dk, batches) - for name in batches: - batch = self._meta['sets']['batches'][name] + batch = meta['sets']['batches'][name] xys = batch['x_y_map'] fs = batch['x_filter_map'] fy = batch['y_filter_map'] my = batch['yks'] - total_len = len(xys) + len(batch['y_on_y']) for idx, xy in enumerate(xys, start=1): x, y = xy diff --git a/quantipy/core/quantify/engine.py b/quantipy/core/quantify/engine.py index cfd07eb0a..a2769f1e2 100644 --- a/quantipy/core/quantify/engine.py +++ b/quantipy/core/quantify/engine.py @@ -64,6 +64,10 @@ def __init__(self, link, weight=None, base_all=False, ignore_flags=False): self.type = self._get_type() if self.type == 'nested': self.nest_def = Nest(self.y, self.d(), self.meta()).nest() + if not self.x == '@': + self.leveled = self.ds.get_property(self.x, 'level') + else: + self.leveled = False self._squeezed = False self.idx_map = None self.xdef = self.ydef = None @@ -250,6 +254,8 @@ def swap(self, var, axis='x', update_axis_def=True, inplace=True): swapped.x, swapped.y = x, y swapped.f, swapped.w = f, w swapped.type = swapped._get_type() + if swapped.type == 'nested': + swapped.nest_def = Nest(swapped.y, swapped.d(), swapped.meta()).nest() swapped._get_matrix() if not update_axis_def and array_swap: swapped.x = org_name @@ -2347,3 +2353,75 @@ def _interlock_texts(self): interlocked_valtexts = list(product(*all_valtexts)) interlocked_qtexts = list(product(*all_qtexts)) return interlocked_qtexts, interlocked_valtexts + +class Level(object): + """ + """ + def __init__(self, quantity): + """ + """ + self.quantity = quantity + self.dataset = self.quantity.ds + self._lvlspec = self.dataset.get_property(self.quantity.x, 'level') + self.array = self._lvlspec['source'] + self.level_codes = self._lvlspec['level_codes'] + self.item_look = self._lvlspec['item_look'] + self._auxdf = self.quantity.count(margin=False).result.reset_index() + self._collapse_codes() + self.lvldf = None + + def _reindex(self, like='freq'): + ds = self.dataset + like_item = self.item_look + itemres = self.quantity.swap(like_item, axis='x', inplace=False) + if like == 'freq': + itemres.count(margin=False, axis=None, as_df=True) + self.lvldf = self.lvldf.reindex(ds.codes(like_item)) + elif like == 'base': + itemres.count(margin=False, axis='x', as_df=True) + x = [self.quantity.x] + vals = itemres.result.index.get_level_values(1).tolist() + idx = pd.MultiIndex.from_product([x, vals], + names=['Question', 'Values']) + self.lvldf.index = idx + None + + def _collapse_codes(self): + df = self._auxdf + for org, lvls in self.level_codes.items(): + for lvl in lvls: + df['Values'] = df['Values'].replace( + lvl, int(org), inplace=False) + return None + + def count(self): + """ + """ + df = self._auxdf.set_index(['Question', 'Values']) + self.lvldf = df.sum(level=1, axis=0) + self._reindex() + return None + + def base(self): + """ + """ + df = self._auxdf.set_index(['Question', 'Values']) + self.lvldf = df.sum(level=0, axis=0) + self._reindex(like='base') + return None + + def percent(self): + """ + """ + self.count() + c = self.lvldf + self.base() + b = self.lvldf + pcts = c.values / b.values * 100 + self.lvldf = pd.DataFrame(pcts, index=c.index, columns=c.columns) + return None + + def as_view(self): + """ + """ + pass \ No newline at end of file diff --git a/quantipy/core/stack.py b/quantipy/core/stack.py index d65bc59b0..7ebed7301 100644 --- a/quantipy/core/stack.py +++ b/quantipy/core/stack.py @@ -738,7 +738,6 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, "You cannot pass both 'variables' and 'x' and/or 'y' to stack.add_link() " "at the same time." ) - x = self._force_key_as_list(x) y = self._force_key_as_list(y) @@ -778,7 +777,8 @@ def add_link(self, data_keys=None, filters=['no_filter'], x=None, y=None, continue else: dataset = qp.DataSet('stack') - dataset.from_components(self[dk].data, self[dk].meta) + dataset.from_components(self[dk].data, self[dk].meta, + reset=False) f_dataset = dataset.filter(filter_def, logic, inplace=False) self[dk][filter_def].data = f_dataset._data self[dk][filter_def].meta = f_dataset._meta @@ -1972,6 +1972,7 @@ def aggregate(self, views, unweighted_base=True, categorize=[], xs = [x for x in x_y_f_w_map.keys() if x in x_in_stack] else: xs = [x for x in xs if x in x_in_stack or isinstance(x, tuple)] + v_typ = self.variable_types(dk, verbose=False) numerics = v_typ['int'] + v_typ['float'] masks = self[dk].meta['masks'] @@ -2395,10 +2396,12 @@ def _check_and_update_calc(calc_expression, text_key): raise ValueError(msg.format(v)) else: check_on.append(v) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] - + for b in _batches: + batch = meta['sets']['batches'][b] + transposed = batch.get('transposed_arrays', batch['transposed']) + if v in transposed: + on_vars += [('@', v)] + break if not only_recode: all_batches = copy.deepcopy(meta['sets']['batches']) for n, b in all_batches.items(): @@ -2646,9 +2649,12 @@ def _add_factors(v, meta, values, args): check_on = list(set(check_on + [items[0]])) else: check_on = list(set(check_on + [v])) - if any(v in meta['sets']['batches'][b]['transposed_arrays'] - for b in _batches): - on_vars += [('@', v)] + for b in _batches: + batch = meta['sets']['batches'][b] + transposed = batch.get('transposed_arrays', batch['transposed']) + if v in transposed: + on_vars += [('@', v)] + break ds = qp.DataSet(dk, dimensions_comp=meta['info'].get('dimensions_comp')) ds.from_stack(self, dk) diff --git a/quantipy/core/tools/qp_decorators.py b/quantipy/core/tools/qp_decorators.py index c427c85fe..3831dc996 100644 --- a/quantipy/core/tools/qp_decorators.py +++ b/quantipy/core/tools/qp_decorators.py @@ -26,6 +26,10 @@ def _var_in_ds(func, *args, **kwargs): all_args = getargspec(func)[0] ds = args[0] for variable, collection in variables.items(): + nested = False + if collection.endswith('_nested'): + nested = True + collection = collection.split('_')[0] # get collection for argument if collection == 'both': collection = ['columns', 'masks'] @@ -35,10 +39,21 @@ def _var_in_ds(func, *args, **kwargs): # get the variable argument to check v_index = all_args.index(variable) var = kwargs.get(variable, args[v_index]) - if var is None: return func(*args, **kwargs) - if not isinstance(var, list): var = [var] + if var is None: + return func(*args, **kwargs) + if not isinstance(var, list): + var = [var] + if nested: + valid = [] + for v in var: + if ' > ' in v: + valid.extend(v.replace(' ', '').split('>')) + else: + valid.append(v) + else: + valid = var # check the variable - not_valid = [v for v in var if not v in c + ['@']] + not_valid = [v for v in valid if not v in c + ['@']] if not_valid: msg = "'{}' argument for {}() must be in {}.\n" msg += '{} is not in {}.' @@ -57,9 +72,14 @@ def _var_is_cat(func, *args, **kwargs): var = kwargs.get(cat, args[v_index]) if var is None: return func(*args, **kwargs) if not isinstance(var, list): var = [var] - var = [v for v in var if not v == '@'] + valid = [] + for v in var: + if ' > ' in v: + valid.extend(v.replace(' ', '').split('>')) + elif not '@' == v: + valid.append(v) # check if varaibles are categorical - not_cat = [v for v in var if not ds._has_categorical_data(v)] + not_cat = [v for v in valid if not ds._has_categorical_data(v)] if not_cat: msg = "'{}' argument for {}() must reference categorical " msg += 'variable.\n {} is not categorical.' diff --git a/quantipy/core/view_generators/view_maps.py b/quantipy/core/view_generators/view_maps.py index fabd4ebd8..b8bb36433 100644 --- a/quantipy/core/view_generators/view_maps.py +++ b/quantipy/core/view_generators/view_maps.py @@ -23,6 +23,7 @@ import quantipy as qp from quantipy.core.cache import Cache +from quantipy.core.quantify.engine import Level import time class QuantipyViews(ViewMapper): @@ -329,7 +330,16 @@ def frequency(self, link, name, kwargs): if q.type == 'array' and not q.y == '@': pass else: - if logic is not None: + if q.leveled: + leveled = Level(q) + if rel_to is not None: + leveled.percent() + elif axis == 'x': + leveled.base() + else: + leveled.count() + view.dataframe = leveled.lvldf + elif logic is not None: try: q.group(groups=logic, axis=axis, expand=expand, complete=complete) except NotImplementedError, e: @@ -361,11 +371,13 @@ def frequency(self, link, name, kwargs): method_nota = 'f' notation = view.notation(method_nota, condition) view._notation = notation - if q.type == 'array': - view.dataframe = q.result.T if link.y == '@' else q.result - else: - view.dataframe = q.result + if not q.leveled: + if q.type == 'array': + view.dataframe = q.result.T if link.y == '@' else q.result + else: + view.dataframe = q.result view._kwargs['exclude'] = q.miss_x + link[notation] = view def descriptives(self, link, name, kwargs): diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 5e0c2b1f7..c4897e246 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1802,8 +1802,19 @@ def __init__(self, chain): self._org_col.get_level_values(1))} self.df.columns = self._edit_col self.array_mi = c._array_style == 0 + self.nested_y = c._nested_y + self._nest_mul = self._nesting_multiplier() return None + def _nesting_multiplier(self): + """ + """ + levels = self._org_col.nlevels + if levels == 2: + return 1 + else: + return (levels / 2) + 1 + def _insert_viewlikes(self, new_index_flat, org_index_mapped): inserts = [new_index_flat.index(val) for val in new_index_flat if not val in org_index_mapped.values()] @@ -1844,7 +1855,9 @@ def _updated_index_tuples(self, axis): if org_tuples[idx][1] == merged_val: new_tuples.append(org_tuples[idx]) else: - new_tuples.append(('*', merged_val)) + empties = ['*'] * self._nest_mul + new_tuple = tuple(empties + [merged_val]) + new_tuples.append(new_tuple) d += 1 i += 1 return new_tuples @@ -1857,6 +1870,7 @@ def _reindex(self): x_names = y_names else: x_names = ['Array', 'Questions'] + if self.nested_y: y_names = y_names * (self._nest_mul - 1) tuples = self._updated_index_tuples(axis=1) self.df.columns = pd.MultiIndex.from_tuples(tuples, names=y_names) tuples = self._updated_index_tuples(axis=0) diff --git a/tests/test_batch.py b/tests/test_batch.py index 0291bd8f1..3aafec021 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -49,7 +49,7 @@ def test_dataset_add_batch(self): batch1 = dataset.add_batch('batch1') batch2 = dataset.add_batch('batch2', 'c', 'weight', .05) self.assertTrue(isinstance(batch1, qp.Batch)) - self.assertEqual(len(_get_meta(batch1).keys()), 32) + self.assertEqual(len(_get_meta(batch1).keys()), 31) b_meta = _get_meta(batch2) self.assertEqual(b_meta['name'], 'batch2') self.assertEqual(b_meta['cell_items'], ['c']) @@ -60,13 +60,16 @@ def test_dataset_get_batch(self): batch, ds = _get_batch('test', full=True) self.assertRaises(KeyError, ds.get_batch, 'name') b = ds.get_batch('test') - attr = ['xks', 'yks', 'filter', 'filter_names', - 'x_y_map', 'x_filter_map', 'y_on_y', - 'forced_names', 'summaries', 'transposed_arrays', 'verbatims', - 'extended_yks_global', 'extended_yks_per_x', - 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', - 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language', 'name', 'total'] + attr = [ + 'xks', 'yks', '_variables', 'filter', 'filter_names', + 'x_y_map', 'x_filter_map', 'y_on_y', 'y_on_y_filter', + 'forced_names', 'transposed', 'leveled', 'verbatims', + 'extended_yks_per_x', + 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', + 'cell_items', 'weights', 'sigproperties', 'additional', + 'sample_size', 'language', 'name', 'skip_items', 'total', + 'unwgt_counts', 'y_filter_map', 'build_info', + '_section_starts'] for a in attr: self.assertEqual(batch.__dict__[a], b.__dict__[a]) @@ -103,7 +106,6 @@ def test_add_downbreak(self): u'q14r03c01', u'q14r04c01', u'q14r05c01', u'q14r06c01', u'q14r07c01', u'q14r08c01', u'q14r09c01', u'q14r10c01']) self.assertEqual(b_meta['forced_names'], {'q3': 'q3_label', 'q5': 'q5_label'}) - self.assertEqual(b_meta['summaries'], ['q5', 'q14_1']) x_y_map = [('q1', ['@']), ('q2', ['@']), ('q2b', ['@']), ('q3', ['@']), ('q4', ['@']), ('q5', ['@']), (u'q5_1', ['@']), (u'q5_2', ['@']), @@ -169,11 +171,13 @@ def test_copy(self): batch2 = batch1.clone('test_copy') batch3 = batch1.clone('test_copy2', as_addition=True) attributes = ['xks', 'yks', 'filter', 'filter_names', 'x_y_map', - 'x_filter_map', 'y_on_y', 'forced_names', 'summaries', - 'transposed_arrays', 'extended_yks_global', 'extended_yks_per_x', + 'x_filter_map', 'y_on_y', 'forced_names', + 'transposed', 'extended_yks_per_x', 'exclusive_yks_per_x', 'extended_filters_per_x', 'meta_edits', 'cell_items', 'weights', 'sigproperties', 'additional', - 'sample_size', 'language'] + 'sample_size', 'language', '_variables', 'y_on_y_filter', + 'leveled', 'skip_items', 'total', 'unwgt_counts', + '_section_starts'] for a in attributes: value = batch1.__dict__[a] value2 = batch2.__dict__[a] @@ -212,20 +216,42 @@ def test_set_sigtest(self): batch.set_sigtests(.05) self.assertEqual(_get_meta(batch)['sigproperties']['siglevels'], [0.05]) - def test_make_summaries_transpose_arrays(self): - batch, ds = _get_batch('test') + def test_level(self): + batch, ds = _get_batch('test', full=True) + batch.add_downbreak(['q5', 'q6']) + batch.level(['q5', 'q6']) + b_meta = _get_meta(batch) + leveled = { + 'q5': ['@', 'gender', 'q2'], + 'q6': ['@', 'gender', 'q2']} + self.assertEqual(b_meta['leveled'], leveled) + + def test_transpose(self): + batch, ds = _get_batch('test', full=True) + batch.add_downbreak(['q5', 'q6']) + batch.transpose(['q5']) + batch.exclusive_arrays(['q5', 'q6']) b_meta = _get_meta(batch) - batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) - batch.make_summaries(None) - self.assertEqual(b_meta['summaries'], []) - batch.transpose_arrays(['q5', 'q6'], False) - batch.transpose_arrays(['q14_2', 'q14_3'], True) - self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) - t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} - self.assertEqual(b_meta['transposed_arrays'], t_a) - batch.make_summaries('q5') - self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) - self.assertRaises(ValueError, batch.make_summaries, 'q7') + x_y_map = [ + ('q5', ['@']), + ('@', ['q5']), + ('q6', ['@'])] + self.assertEqual(b_meta['x_y_map'], x_y_map) + + # def test_make_summaries_transpose_arrays(self): + # batch, ds = _get_batch('test') + # b_meta = _get_meta(batch) + # batch.add_downbreak(['q5', 'q6', 'q14_2', 'q14_3', 'q14_1']) + # batch.make_summaries(None) + # self.assertEqual(b_meta['summaries'], []) + # batch.transpose_arrays(['q5', 'q6'], False) + # batch.transpose_arrays(['q14_2', 'q14_3'], True) + # self.assertEqual(b_meta['summaries'], ['q5', 'q6', 'q14_2', 'q14_3']) + # t_a = {'q14_2': True, 'q14_3': True, 'q5': False, 'q6': False} + # self.assertEqual(b_meta['transposed_arrays'], t_a) + # batch.make_summaries('q5') + # self.assertEqual(b_meta['transposed_arrays'], {'q5': False}) + # self.assertRaises(ValueError, batch.make_summaries, 'q7') def test_extend_y(self): batch1, ds = _get_batch('test1', full=True) @@ -235,26 +261,25 @@ def test_extend_y(self): self.assertRaises(ValueError, batch1.extend_y, 'q2b', 'q5') batch1.extend_y('q2b') x_y_map = [('q1', ['@', 'gender', 'q2', 'q2b']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q2b']), - (u'q6_2', ['@', 'gender', 'q2', 'q2b']), - (u'q6_3', ['@', 'gender', 'q2', 'q2b']), - ('age', ['@', 'gender', 'q2', 'q2b'])] + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q2b']), + (u'q6_2', ['@', 'gender', 'q2', 'q2b']), + (u'q6_3', ['@', 'gender', 'q2', 'q2b']), + ('age', ['@', 'gender', 'q2', 'q2b'])] self.assertEqual(b_meta1['x_y_map'], x_y_map) - self.assertEqual(b_meta1['extended_yks_global'], ['q2b']) batch2.extend_y('q2b', 'q2') batch2.extend_y('q3', 'q6') extended_yks_per_x = {u'q6_3': ['q3'], 'q2': ['q2b'], u'q6_1': ['q3'], - u'q6_2': ['q3'], 'q6': ['q3']} + u'q6_2': ['q3']} self.assertEqual(b_meta2['extended_yks_per_x'], extended_yks_per_x) x_y_map = [('q1', ['@', 'gender', 'q2']), - ('q2', ['@', 'gender', 'q2', 'q2b']), - ('q6', ['@']), - (u'q6_1', ['@', 'gender', 'q2', 'q3']), - (u'q6_2', ['@', 'gender', 'q2', 'q3']), - (u'q6_3', ['@', 'gender', 'q2', 'q3']), - ('age', ['@', 'gender', 'q2'])] + ('q2', ['@', 'gender', 'q2', 'q2b']), + ('q6', ['@']), + (u'q6_1', ['@', 'gender', 'q2', 'q3']), + (u'q6_2', ['@', 'gender', 'q2', 'q3']), + (u'q6_3', ['@', 'gender', 'q2', 'q3']), + ('age', ['@', 'gender', 'q2'])] self.assertEqual(b_meta2['x_y_map'], x_y_map) def test_replace_y(self): @@ -264,8 +289,7 @@ def test_replace_y(self): batch.replace_y('q2b', 'q6') exclusive_yks_per_x = {u'q6_3': ['@', 'q2b'], u'q6_1': ['@', 'q2b'], - u'q6_2': ['@', 'q2b'], - 'q6': ['@', 'q2b']} + u'q6_2': ['@', 'q2b']} self.assertEqual(b_meta['exclusive_yks_per_x'], exclusive_yks_per_x) x_y_map = [('q1', ['@', 'gender', 'q2']), ('q2', ['@', 'gender', 'q2']), diff --git a/tests/test_stack.py b/tests/test_stack.py index d461b6dac..1fd410858 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1115,11 +1115,10 @@ def test_stack_aggregate(self): b2.add_downbreak(['q1', 'q6']) b2.add_crossbreak(['gender', 'q2']) b2.set_weights('weight_b') - b2.transpose_arrays('q6', True) + b2.transpose('q6') b3.add_downbreak(['q1', 'q7']) b3.add_crossbreak(['q2b']) b3.add_y_on_y('y_on_y') - b3.make_summaries(None) b3.set_weights(['weight_a', 'weight_b']) stack = ds.populate(verbose=False) stack.aggregate(['cbase', 'counts', 'c%'], True, @@ -1131,19 +1130,21 @@ def test_stack_aggregate(self): 'x|f|:|y|weight_a|c%', 'x|f|:|y|weight_b|c%', 'x|f|:||weight_a|counts', 'x|f|:||weight_b|counts', 'x|f|x:||weight_a|cbase', 'x|f|x:||weight_b|cbase', 'x|f|x:|||cbase'] - cols = ['@', 'age', 'q1', 'q2b', 'q6', u'q6_1', u'q6_2', u'q6_3', u'q7_1', + cols = ['@', 'age', 'q1', 'q2b', 'q6', u'q6_1', u'q6_2', u'q6_3', u'q7', u'q7_1', u'q7_2', u'q7_3', u'q7_4', u'q7_5', u'q7_6'] - values = [['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 'NONE', 4.0, 2.0, 'NONE', 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], - [1.0, 3.0, 6.0, 'NONE', 1.0, 6.0, 6.0, 6.0, 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE']] + values = [ + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 'NONE', 2.0, 2.0, 'NONE', 'NONE', 'NONE', 'NONE', 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + ['NONE', 3.0, 5.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0,2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 'NONE', 4.0, 2.0, 1.0, 3.0, 3.0, 3.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], + [1.0, 3.0, 6.0, 'NONE', 2.0, 6.0, 6.0, 6.0, 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE', 'NONE']] + describe = stack.describe('view', 'x').replace(numpy.NaN, 'NONE') self.assertEqual(describe.index.tolist(), index) self.assertEqual(describe.columns.tolist(), cols) From 27fe1ebdac445c5113fb6f1c9f6b52221fd64727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Apr 2019 11:11:22 +0200 Subject: [PATCH 679/733] fix filter handling in batch --- quantipy/core/batch.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index b7b2ae8f9..05e7bc7c5 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -802,6 +802,7 @@ def remove_filter(self): self.filter_names = [] self.extended_filters_per_x = {} self.y_on_y_filter = {} + self.y_filter_map = {} self._update() return None @@ -1127,8 +1128,9 @@ def _map_y_on_y_filter(self): ------- None """ - self.y_filter_map = {} for y_on_y in self.y_on_y: + if y_on_y in self.y_filter_map: + continue ext_rep, y_f = self.y_on_y_filter[y_on_y] logic = {'label': y_on_y, 'logic': y_f} if ext_rep == 'replace': @@ -1326,6 +1328,8 @@ def _get_vlist(self, batch, mode): for oe in var[:]: oes += oe["break_by"] + oe["columns"] + [oe["filter"]] var = oes + if key == "f": + var = batch["filter_names"] + batch["y_filter_map"].values() if not isinstance(var, list): var = [var] for v in var: if v and v in self and v not in vlist: From 3ef5122f19ef6674667f4af0a336099365f81c13 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 23 Apr 2019 15:25:24 +0200 Subject: [PATCH 680/733] fixing the bug --- quantipy/core/batch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 3a4f59aff..cbaaf0c0d 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -564,6 +564,7 @@ def sections(self): sects = self._section_starts full_sections = OrderedDict() rev_full_sections = OrderedDict() + last_group = None for x in self.xks: if x in sects: full_sections[x] = sects[x] @@ -575,6 +576,8 @@ def sections(self): rev_full_sections[v].append(k) else: rev_full_sections[v] = [k] + if None in rev_full_sections.keys(): + del rev_full_sections[None] return rev_full_sections @verify(variables={'name': 'columns'}, categorical='name') From 85afb7b72eac04f8ede88fcfb5f1ad8609f0f9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 24 Apr 2019 09:59:18 +0200 Subject: [PATCH 681/733] adjust to_dataset --- quantipy/core/batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 05e7bc7c5..046de4079 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1198,7 +1198,7 @@ def _samplesize_from_batch_filter(self): return None @modify(to_list=["mode", "misc"]) - def to_dataset(self, mode=None, from_set=None, additions="sort_within", + def to_dataset(self, mode=None, from_set="data file", additions="sort_within", manifest_edits="keep", integrate_rc=(["_rc", "_rb"], True), misc=["RecordNo", "caseid", "identity"]): """ @@ -1225,6 +1225,8 @@ def to_dataset(self, mode=None, from_set=None, additions="sort_within", if additions == "sort_between": for add in adds: vlist += self._get_vlist(batches[add], mode) + if not from_set: + from_set = vlist vlist = self.align_order(vlist, from_set, integrate_rc, fix=misc) if additions == "sort_within": for add in adds: From a463b3be8ec1f6abf9237a951ae741f73c782566 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Wed, 24 Apr 2019 11:06:32 +0200 Subject: [PATCH 682/733] forcing the ci parameter for array summaries --- quantipy/sandbox/sandbox.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index c4897e246..35130ef58 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -707,6 +707,13 @@ def cut(self, values, ci=None, base=False, tests=False): if base and not 'All' in values: values = ['All'] + values for c in self.chains: + # force ci parameter for proper targeting on array summaries... + if c.array_style == 0 and ci is None: + _ci = c.cell_items.split('_')[0] + if not _ci.startswith('counts'): + ci = '%' + else: + ci = 'counts' if c.sig_test_letters: c._remove_letter_header() idxs, names, order = c._view_idxs( values, keep_tests=tests, keep_bases=base, names=True, ci=ci) @@ -2422,6 +2429,7 @@ def _view_idxs(self, view_tags, keep_tests=True, keep_bases=True, names=False, c rp_idx = self._row_pattern(ci)[0] rowmeta = rowmeta[rp_idx] else: + rp_idx = 0 rowmeta = rowmeta[0] rows = [] for r in rowmeta: From e80b6afb9de14d742c95967f8fc0926416d749e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 14 May 2019 14:25:19 +0200 Subject: [PATCH 683/733] add missing parameter in pptx parameter_map --- quantipy/sandbox/pptx/PptxDefaultsClass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/sandbox/pptx/PptxDefaultsClass.py b/quantipy/sandbox/pptx/PptxDefaultsClass.py index 747f05647..ce31d8119 100644 --- a/quantipy/sandbox/pptx/PptxDefaultsClass.py +++ b/quantipy/sandbox/pptx/PptxDefaultsClass.py @@ -110,6 +110,7 @@ def update_shape(self, shape, settings): """ parameter_map = {'shapes': self._shapes, 'charts': self._charts, + 'tables': self._tables, 'textboxes': self._textboxes, 'chart_bar': self._chart_bar, 'chart_bar_stacked100': self._chart_bar_stacked100, From beb820d8802e5bd8e38cac5066ac69beed86de10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 16 May 2019 07:42:56 +0200 Subject: [PATCH 684/733] bugfix in batch --- quantipy/core/batch.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index b5238dccc..006e927f9 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -534,15 +534,16 @@ def extend_x(self, ext_xks): msg = '{}: Cannot use an array item as position' raise ValueError(msg.format(pos)) if not isinstance(var, list): var = [var] - for v in var: + for v in self.unroll(var, both="all"): if v in self.xks: msg = '{} is already included as downbreak.' raise ValueError(msg.format(v)) self.xks.insert(self.xks.index(pos), v) else: - if x not in self: - raise KeyError('{} is not included.'.format(x)) - self.xks.append(x) + for var in self.unroll(x, both="all"): + if var not in self: + raise KeyError('{} is not included.'.format(var)) + self.xks.append(var) self._update() return None @@ -1254,6 +1255,9 @@ def to_dataset(self, mode=None, from_set=None, additions="sort_within", ds = qp.DataSet(self.name, self._dimensions_comp) ds.from_components(self._data.copy(), org_copy.deepcopy(self._meta), True, self.language) + for b in ds.batches(): + if not (b in adds or b == ds.name): + del ds._meta['sets']['batches'][b] if merge_f: ds.merge_filter(f, filters) From 10d6cf95a07bb9ce4e36c246ac65ef175ec3009c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 20 May 2019 12:06:23 +0200 Subject: [PATCH 685/733] add pytest_cache to gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 945fc4981..e14a58638 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,5 @@ docs/API/_templates/ *.egg .eggs/ .cache -.pytest_cache/ -.pytest_cache/v/cache/nodeids - +.pytest_cache/* build From a10b8c091f4d6aa6b5b854e552c82cefabfe43b2 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Mon, 20 May 2019 17:24:05 +0200 Subject: [PATCH 686/733] hack that turns qp logic expressions intp pd eval str from dummy variables --- quantipy/core/dataset.py | 9 +++++- quantipy/core/weights/rim.py | 14 ++++++-- quantipy/core/weights/weight_engine.py | 44 ++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 8ff3269ca..498caf3d2 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2886,9 +2886,16 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', meta, data = ds.split() else: meta, data = self.split() - engine = qp.WeightEngine(data, meta) + engine = qp.WeightEngine(data, meta=meta) engine.add_scheme(weight_scheme, key=unique_key, verbose=verbose) + + # =================================================== + eds = engine.dataset + if any('__logic_dummy__' in v for v in eds.singles()): + self._meta, self._data = eds.split() engine.run() + # =================================================== + org_wname = weight_name if report: print engine.get_report() diff --git a/quantipy/core/weights/rim.py b/quantipy/core/weights/rim.py index bd96d124b..3f32392c2 100644 --- a/quantipy/core/weights/rim.py +++ b/quantipy/core/weights/rim.py @@ -9,6 +9,14 @@ import warnings import time +from quantipy.core.tools.view.logic import ( + has_any, has_all, has_count, + not_any, not_all, not_count, + is_lt, is_ne, is_gt, + is_le, is_eq, is_ge, + union, intersection, get_logic_index) + + class Rim: def __init__(self, name, @@ -44,6 +52,7 @@ def __init__(self, # Constants self._FILTER_DEF = 'filters' + self._FILTER_VARS = 'filter_vars' self._TARGETS = 'targets' self._TARGETS_INDEX = 'targets_index' self._REPORT = 'report' @@ -238,7 +247,7 @@ def _get_wdf(self, group): weight_var = self._weight_name() if filters is not None: wdf = self._df.copy().query(filters) - filter_vars = list(set(self._get_group_filter_cols(filters))) + filter_vars = self.groups[group][self._FILTER_VARS] selected_cols = target_vars + filter_vars + [weight_var] else: wdf = self._df.copy() @@ -282,8 +291,7 @@ def impute_method(self, method, target=None): self._specific_impute[target] = method def _get_scheme_filter_cols(self): - scheme_filter_cols = [self._get_group_filter_cols( - self.groups[group][self._FILTER_DEF]) + scheme_filter_cols = [self.groups[group][self._FILTER_VARS] for group in self.groups] scheme_filter_cols = list(set([filter_col for sublist in scheme_filter_cols diff --git a/quantipy/core/weights/weight_engine.py b/quantipy/core/weights/weight_engine.py index 30594cc24..5e82a67db 100644 --- a/quantipy/core/weights/weight_engine.py +++ b/quantipy/core/weights/weight_engine.py @@ -4,6 +4,9 @@ import pandas as pd from rim import Rim from collections import OrderedDict +import re + +from quantipy.core.dataset import DataSet class WeightEngine: @@ -19,7 +22,7 @@ def __init__(self, "\n You must pass a pandas.DataFrame to the 'data' argument of the WeightEngine" "\n constructor. If your DataFrame is serialized please load it first." ) - + self.dataset = None self._df = data.copy() self.schemes = {} @@ -41,6 +44,9 @@ def __init__(self, "\n constructor. If your meta is serialized please load it first." ) self._meta = meta + self.dataset = DataSet('__weights__', False) + self.dataset.from_components(self._df.copy(), self._meta) + def get_report(self): """ @@ -151,7 +157,7 @@ def run(self, schemes=[]): weights = the_scheme._compute() self._df[the_scheme._weight_name()] = weights - + else: raise Exception(("Scheme '%s' not found." % scheme)) else: @@ -187,5 +193,39 @@ def dataframe(self, scheme=None): def add_scheme(self, scheme, key, verbose=True): if scheme.name in self.schemes: print "Overwriting existing scheme '%s'." % scheme.name + self._resolve_filters(scheme) self.schemes[scheme.name] = {self._SCHEME: scheme, self._KEY: key} scheme._minimize_columns(self._df, key, verbose) + + def _resolve_filters(self, scheme): + grps = scheme.groups + for grp in grps: + f = grps[grp]['filters'] + if f is not None: + grps[grp]['filter_vars'] = self._find_filter_variables(f) + if not isinstance(f, (str, unicode)): + f = self._replace_logical_filter(f, grp) + msg = 'Converted {} filter to logical dummy expression: {}' + print msg.format(grp, f) + grps[grp]['filter_vars'].append(f.split('=')[0]) + grps[grp]['filters'] = f + return None + + def _find_filter_variables(self, filter_expression): + """ + """ + filter_variables = [] + for col in self._df.columns: + if re.search(r"\b"+col+r"\b", unicode(filter_expression)): + filter_variables.append(col) + return filter_variables + + def _replace_logical_filter(self, filter_expression, grp_name): + """ + """ + varname = '{}__logic_dummy__'.format(grp_name) + category = [(1, 'select', filter_expression)] + meta = (varname, 'single', '', category) + self.dataset.derive(*meta) + self._df[varname] = self.dataset._data[varname].copy() + return '{}==1'.format(varname) From f816c45f1c38bb297a1a051960c5830367021395 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 21 May 2019 10:54:14 +0200 Subject: [PATCH 687/733] repairing weight engine test data... --- quantipy/core/weights/rim.py | 10 +- tests/engine_B_data.csv | 42 +-- tests/engine_B_meta.json | 563 +---------------------------------- 3 files changed, 28 insertions(+), 587 deletions(-) diff --git a/quantipy/core/weights/rim.py b/quantipy/core/weights/rim.py index 3f32392c2..57f18b542 100644 --- a/quantipy/core/weights/rim.py +++ b/quantipy/core/weights/rim.py @@ -67,6 +67,7 @@ def __init__(self, self.groups[self._DEFAULT_NAME] = {} self.groups[self._DEFAULT_NAME][self._REPORT] = None self.groups[self._DEFAULT_NAME][self._FILTER_DEF] = None + self.groups[self._DEFAULT_NAME][self._FILTER_VARS] = [] self.groups[self._DEFAULT_NAME][self._TARGETS] = self._empty_target_list() self.groups[self._DEFAULT_NAME][self._TARGETS_INDEX] = None self.groups[self._DEFAULT_NAME][self._ITERATIONS_] = None @@ -352,11 +353,12 @@ def group_targets(self, group_targets): None """ if isinstance(group_targets, dict): + if all (group_targets[group] < 1 for group in group_targets): + div_by = 1.0 + else: + div_by = 100.0 for group in group_targets: - if group_targets[group] < 1: - self._group_targets[group] = group_targets[group] - else: - self._group_targets[group] = group_targets[group] / 100.0 + self._group_targets[group] = group_targets[group] / div_by else: raise ValueError(('Group_targets must be of type %s NOT %s ') % \ (type({}), type(group_targets))) diff --git a/tests/engine_B_data.csv b/tests/engine_B_data.csv index 9113de1b3..f1874a472 100644 --- a/tests/engine_B_data.csv +++ b/tests/engine_B_data.csv @@ -1,21 +1,21 @@ -identity,cost_breakfast,cost_lunch,cost_dinner,name,endtime,duration,profile_gender,age,age_group,q1_1,q1_2,q1_3,q1_4,q1_5,q2_1,q2_2,q2_3,q3_1,q3_2,q3_3,q4,weight -6542497,3.25,4.23,6.99,Alfred Pope,02/07/2014 16:58,,1,51,6,1,2,1,2,1,1,1,2,1,2,,1;2;,0.5 -6542644,3.69,4.3,5.67,Devin Watts,02/07/2014 16:58,,2,52,6,1,1,1,3,2,2,1,2,2,,,2;,0.5 -6542856,3.67,4.67,6.2,Guadalupe Mathis,02/07/2014 16:59,,1,17,2,2,2,2,3,1,1,1,1,1,2,3,1;2;3;,0.5 -6543060,4.32,4.45,6.21,Candice Stokes,02/07/2014 16:59,,1,47,5,1,3,1,1,2,2,1,2,2,,,2;,0.5 -6543208,3.52,6.44,6.5,Jerry Freeman,02/07/2014 17:00,,1,46,5,1,3,2,2,1,1,2,2,1,,,1;,0.5 -6543287,3.65,4.08,4.81,Kay Hamilton,02/07/2014 17:01,,2,43,5,2,1,3,1,1,1,2,1,1,3,,1;3;,0.5 -6543371,4.74,5.36,7.07,Conrad Graves,02/07/2014 17:01,,2,57,6,1,2,2,2,1,2,1,2,2,,,2;,0.5 -6543622,4.7,7.66,9.91,Ebony Kelly,02/07/2014 17:01,,1,50,6,2,1,1,2,2,1,2,2,1,,,1;,0.5 -6543719,4.67,5.47,7.03,Andre Robinson,02/07/2014 17:02,,2,48,5,3,2,1,1,1,1,2,1,1,3,,1;3;,0.5 -6543729,3.45,4.17,7.01,Matt Wallace,02/07/2014 17:02,,2,7,1,3,1,2,1,2,1,1,1,1,2,3,1;2;3;,0.5 -6543841,3.97,5.71,8.32,Peter Richardson,02/07/2014 17:02,,1,22,3,1,2,1,2,3,2,1,2,2,,,2;,0.5 -6543983,4.11,6.87,7.26,Rudy Erickson,02/07/2014 17:02,,2,1,1,2,3,2,1,3,1,2,2,1,,,1;,0.5 -6544213,3.93,4.52,4.94,Pablo Romero,02/07/2014 17:03,,2,11,2,1,3,3,1,1,1,2,1,1,3,,1;3;,0.5 -6544442,3.46,4.88,6.48,Brandon Cummings,02/07/2014 17:03,,2,14,2,2,1,3,2,2,1,2,1,1,3,,1;3;,0.5 -6544515,4.34,4.62,7.46,Ivan Wilson,02/07/2014 17:03,,1,3,1,1,2,1,1,1,1,1,2,1,2,,1;2;,0.5 -6544549,4.14,4.77,4.81,Lisa Castro,02/07/2014 17:04,,1,34,4,2,1,2,2,2,2,1,2,2,,,2;,0.5 -6544798,4.71,4.8,4.93,Bertha Collier,02/07/2014 17:05,,2,1,1,1,2,1,3,1,1,2,2,1,,,1;,0.5 -6544827,3.8,4.36,5.69,Angie Hoffman,02/07/2014 17:05,,2,34,4,2,1,2,3,2,1,2,1,1,3,,1;3;,0.5 -6544946,3.68,4.81,6.48,Bethany Jones,02/07/2014 17:07,,1,38,4,3,2,1,1,1,1,2,2,1,,,1;,0.5 -6545105,3.71,4.31,4.77,Wilbur Rios,02/07/2014 17:07,,2,27,3,3,1,2,2,2,1,2,1,1,3,,1;3;,0.5 +,cost_breakfast,cost_lunch,cost_dinner,full_name,endtime,duration,profile_gender,age,age_group,q1_1,q1_2,q1_3,q1_4,q1_5,q2_1,q2_2,q2_3,q3_1,q3_2,q3_3,q4,weight,@1,identity +0,3.25,4.23,6.99,Alfred Pope,2014-02-07 16:58:00,,1,51,6,1,2,1,2,1,1,1,2,1,2.0,,1;2;,0.5,1.0,0 +1,3.69,4.3,5.67,Devin Watts,2014-02-07 16:58:00,,2,52,6,1,1,1,3,2,2,1,2,2,,,2;,0.5,1.0,1 +2,3.67,4.67,6.2,Guadalupe Mathis,2014-02-07 16:59:00,,1,17,2,2,2,2,3,1,1,1,1,1,2.0,3.0,1;2;3;,0.5,1.0,2 +3,4.32,4.45,6.21,Candice Stokes,2014-02-07 16:59:00,,1,47,5,1,3,1,1,2,2,1,2,2,,,2;,0.5,1.0,3 +4,3.52,6.44,6.5,Jerry Freeman,2014-02-07 17:00:00,,1,46,5,1,3,2,2,1,1,2,2,1,,,1;,0.5,1.0,4 +5,3.65,4.08,4.81,Kay Hamilton,2014-02-07 17:01:00,,2,43,5,2,1,3,1,1,1,2,1,1,3.0,,1;3;,0.5,1.0,5 +6,4.74,5.36,7.07,Conrad Graves,2014-02-07 17:01:00,,2,57,6,1,2,2,2,1,2,1,2,2,,,2;,0.5,1.0,6 +7,4.7,7.66,9.91,Ebony Kelly,2014-02-07 17:01:00,,1,50,6,2,1,1,2,2,1,2,2,1,,,1;,0.5,1.0,7 +8,4.67,5.47,7.03,Andre Robinson,2014-02-07 17:02:00,,2,48,5,3,2,1,1,1,1,2,1,1,3.0,,1;3;,0.5,1.0,8 +9,3.45,4.17,7.01,Matt Wallace,2014-02-07 17:02:00,,2,7,1,3,1,2,1,2,1,1,1,1,2.0,3.0,1;2;3;,0.5,1.0,9 +10,3.97,5.71,8.32,Peter Richardson,2014-02-07 17:02:00,,1,22,3,1,2,1,2,3,2,1,2,2,,,2;,0.5,1.0,10 +11,4.11,6.87,7.26,Rudy Erickson,2014-02-07 17:02:00,,2,1,1,2,3,2,1,3,1,2,2,1,,,1;,0.5,1.0,11 +12,3.93,4.52,4.94,Pablo Romero,2014-02-07 17:03:00,,2,11,2,1,3,3,1,1,1,2,1,1,3.0,,1;3;,0.5,1.0,12 +13,3.46,4.88,6.48,Brandon Cummings,2014-02-07 17:03:00,,2,14,2,2,1,3,2,2,1,2,1,1,3.0,,1;3;,0.5,1.0,13 +14,4.34,4.62,7.46,Ivan Wilson,2014-02-07 17:03:00,,1,3,1,1,2,1,1,1,1,1,2,1,2.0,,1;2;,0.5,1.0,14 +15,4.14,4.77,4.81,Lisa Castro,2014-02-07 17:04:00,,1,34,4,2,1,2,2,2,2,1,2,2,,,2;,0.5,1.0,15 +16,4.71,4.8,4.93,Bertha Collier,2014-02-07 17:05:00,,2,1,1,1,2,1,3,1,1,2,2,1,,,1;,0.5,1.0,16 +17,3.8,4.36,5.69,Angie Hoffman,2014-02-07 17:05:00,,2,34,4,2,1,2,3,2,1,2,1,1,3.0,,1;3;,0.5,1.0,17 +18,3.68,4.81,6.48,Bethany Jones,2014-02-07 17:07:00,,1,38,4,3,2,1,1,1,1,2,2,1,,,1;,0.5,1.0,18 +19,3.71,4.31,4.77,Wilbur Rios,2014-02-07 17:07:00,,2,27,3,3,1,2,2,2,1,2,1,1,3.0,,1;3;,0.5,1.0,19 diff --git a/tests/engine_B_meta.json b/tests/engine_B_meta.json index 8b6fa9492..fe13d6d8d 100644 --- a/tests/engine_B_meta.json +++ b/tests/engine_B_meta.json @@ -1,562 +1 @@ -{ - "info": { - "notes": "Client version at 2014/09/25", - "from_source": { - "pandas_reader": "from_csv", - "kwargs": { - "infer_datetime_format": false, - "index_col": 0, - "tupleize_cols": false, - "sep": ",", - "parse_dates": true, - "header": 0, - "path": "./data/example_data_with_meta.csv" - } - }, - "name": "ASDA tracker W4" - }, - "lib": { - "text": { - "dk": [ - "Don't know", - "weiss nicht" - ] - }, - "text map": { - "auto": "enu", - "map": [ - "enu", - "deu" - ] - }, - "values": { - "core": { - "98 Not asked 99 Skipped": [ - { - "text": [ - "Not asked", - "Nicht gefragt" - ], - "value": 98, - "missing": true - }, - { - "text": [ - "Skipped", - "Übersprungen" - ], - "value": 99, - "missing": true - } - ], - "97 dk": [ - { - "text": [ - "lib@text@dk" - ], - "value": 97 - } - ], - "96 none": [ - { - "text": [ - "None", - "Keine" - ], - "value": 96 - } - ], - "Not shown": [ - { - "text": [ - "Not shown", - "Nicht gesehen" - ], - "value": 0, - "missing": true - } - ], - "96 none missing": [ - { - "text": [ - "None", - "Keine" - ], - "value": 96, - "missing": true - } - ], - "95 other": [ - { - "text": [ - "Other", - "Sonstige" - ], - "value": 95 - } - ], - "97 dk missing": [ - { - "text": [ - "lib@text@dk" - ], - "value": 97, - "missing": true - } - ] - }, - "custom": { - "12345 disagree-agree": [ - { - "text": [ - "Disagree", - "Stimme nicht zu" - ], - "value": 1 - }, - { - "text": [ - "Somewhat disagree", - "Stimme eher nicht zu" - ], - "value": 2 - }, - { - "text": [ - "Neither", - "Teils/teils" - ], - "value": 3 - }, - { - "text": [ - "Somewhat agree", - "Stimme eher zu" - ], - "value": 4 - }, - { - "text": [ - "Agree", - "Stimme zu" - ], - "value": 5 - } - ], - "meals": [ - { - "text": [ - "Breakfast", - "Frühstück" - ], - "value": 1 - }, - { - "text": [ - "Lunch", - "Mittagessen" - ], - "value": 2 - }, - { - "text": [ - "Dinner", - "Abendessen" - ], - "value": 3 - } - ], - "dichotomous": [ - { - "text": [ - "Selected", - "Ja" - ], - "value": 1 - }, - { - "text": [ - "Not selected", - "Nein" - ], - "value": 2 - } - ] - } - } - }, - "masks": { - "q1": { - "text": [ - "How much to you agree or disagree with the following statements?", - "Inwieweit stimmen Sie den folgenden Aussagen zu?" - ], - "type": "array", - "items": [ - { - "source": "columns@q1_1" - }, - { - "source": "columns@q1_2" - }, - { - "source": "columns@q1_3" - }, - { - "source": "columns@q1_4" - }, - { - "source": "columns@q1_5" - } - ] - }, - "q2": { - "text": [ - "Which meals do you normally eat?", - "Welche Mahlzeiten nehmen Sie normalerweise zu sich?" - ], - "type": "dichotomous set", - "items": [ - { - "source": "columns@q2_1" - }, - { - "source": "columns@q2_2" - }, - { - "source": "columns@q2_3" - } - ] - }, - "q3": { - "text": [ - "Which meals do you normally eat?", - "Welche Mahlzeiten nehmen Sie normalerweise zu sich?" - ], - "type": "categorical set", - "items": [ - { - "source": "columns@q3_1" - }, - { - "source": "columns@q3_2" - }, - { - "source": "columns@q3_3" - } - ] - }, - "cost": { - "text": [ - "How much did you pay for these meals?", - "Wie viel haben Sie für diese Mahlzeiten bezahlt?" - ], - "type": "array", - "items": [ - { - "source": "columns@cost_breakfast", - "text": [ - "Breakfast", - "Frühstück" - ] - }, - { - "source": "columns@cost_lunch", - "text": [ - "Lunch", - "Mittagessen" - ] - }, - { - "source": "columns@cost_dinner", - "text": [ - "Dinner", - "Abendessen" - ] - } - ] - } - }, - "sets": { - "demographics": { - "text": "PDLs", - "items": [ - "columns@name", - "columns@profile_gender" - ] - }, - "internal": { - "text": "Variables for internal use", - "items": [ - "sets@system", - "columns@profile_gender", - "sets@survey variables" - ] - }, - "client export": { - "text": "Variables for external use", - "items": [ - "columns@profile_gender", - "sets@survey variables" - ] - }, - "system": { - "text": "System variables", - "items": [ - "columns@identity", - "columns@endtime" - ] - }, - "survey variables": { - "text": "Variables from questionnaire", - "items": [ - "masks@cost", - "masks@q1", - "masks@q2", - "masks@q3", - "columns@q4" - ] - } - }, - "type": "pandas.DataFrame", - "columns": { - "cost_dinner": { - "text": [ - "How much did you pay for your dinner?", - "Wie viel haben Sie für ihr Abendessen bezahlt?" - ], - "type": "float" - }, - "cost_lunch": { - "text": [ - "How much did you pay for your lunch?", - "Wie viel haben Sie für ihr Mittagessen bezahlt?" - ], - "type": "float" - }, - "duration": { - "text": [ - "Survey duration", - "Umfragedauer" - ], - "type": "time" - }, - "cost_breakfast": { - "text": [ - "How much did you pay for your breakfast?", - "Wie viel haben Sie für ihr Frühstück bezahlt?" - ], - "type": "float" - }, - "age_group": { - "values": [ - { - "text": "1-9", - "value": 1 - }, - { - "text": "11-19", - "value": 2 - }, - { - "text": "21-29", - "value": 3 - }, - { - "text": "31-39", - "value": 4 - }, - { - "text": "41-49", - "value": 5 - }, - { - "text": "51-59", - "value": 6 - } - ], - "text": [ - "Age group", - "Altersgruppe" - ], - "type": "single" - }, - "endtime": { - "text": [ - "Date of survey.", - "Umfragedatum" - ], - "type": "date" - }, - "q4": { - "values": [ - "lib@values@custom@meals" - ], - "text": [ - "Which meals do you normally eat?", - "Welche Mahlzeiten nehmen Sie normalerweise zu sich?" - ], - "type": "delimited set", - "sep": ";" - }, - "identity": { - "text": [ - "id", - "Identifikationsnummer" - ], - "type": "int" - }, - "q2_1": { - "values": [ - "lib@values@custom@dichotomous" - ], - "text": [ - "Breakfast", - "Frühstück" - ], - "type": "single" - }, - "q2_2": { - "values": [ - "lib@values@custom@dichotomous" - ], - "text": [ - "Lunch", - "Mittagessen" - ], - "type": "single" - }, - "q2_3": { - "values": [ - "lib@values@custom@dichotomous" - ], - "text": [ - "Dinner", - "Abendessen" - ], - "type": "single" - }, - "q3_1": { - "values": [ - "lib@values@custom@meals" - ], - "text": [ - "Meals eaten (1/3)", - "Verzehrte Mahlzeiten (1/3)" - ], - "type": "single" - }, - "q3_3": { - "values": [ - "lib@values@custom@meals" - ], - "text": [ - "Meals eaten (3/3)", - "Verzehrte Mahlzeiten (3/3)" - ], - "type": "single" - }, - "q3_2": { - "values": [ - "lib@values@custom@meals" - ], - "text": [ - "Meals eaten (2/3)", - "Verzehrte Mahlzeiten (2/3)" - ], - "type": "single" - }, - "q1_5": { - "values": [ - "lib@values@custom@12345 disagree-agree", - "lib@values@core@97 dk" - ], - "text": [ - "I like Dr Pepper.", - "Ich mag Dr Pepper" - ], - "type": "single" - }, - "q1_4": { - "values": [ - "lib@values@custom@12345 disagree-agree", - "lib@values@core@97 dk" - ], - "text": [ - "I like 7-Up.", - "Ich mag 7-Up." - ], - "type": "single" - }, - "q1_3": { - "values": [ - "lib@values@custom@12345 disagree-agree", - "lib@values@core@97 dk" - ], - "text": [ - "I like Sprite.", - "Ich mag Sprite." - ], - "type": "single" - }, - "q1_2": { - "values": [ - "lib@values@custom@12345 disagree-agree", - "lib@values@core@97 dk" - ], - "text": [ - "I like Fanta.", - "Ich mag Fanta." - ], - "type": "single" - }, - "q1_1": { - "values": [ - "lib@values@custom@12345 disagree-agree", - "lib@values@core@97 dk" - ], - "text": [ - "I like Coca-Cola.", - "Ich mag Coca-Cola." - ], - "type": "single" - }, - "name": { - "text": [ - "What is your name?", - "Wie heißen Sie?" - ], - "type": "string" - }, - "age": { - "text": [ - "Age", - "Alter" - ], - "type": "int" - }, - "profile_gender": { - "values": [ - { - "text": [ - "Male", - "Männlich" - ], - "value": 1 - }, - { - "text": [ - "Female", - "Weiblich" - ], - "value": 2 - } - ], - "text": [ - "Gender", - "Geschlecht" - ], - "type": "single" - } - } -} \ No newline at end of file +{"columns": {"@1": {"type": "int"}, "age": {"text": ["Age", "Alter"], "type": "int"}, "age_group": {"text": ["Age group", "Altersgruppe"], "type": "single", "values": [{"text": "1-9", "value": 1}, {"text": "11-19", "value": 2}, {"text": "21-29", "value": 3}, {"text": "31-39", "value": 4}, {"text": "41-49", "value": 5}, {"text": "51-59", "value": 6}]}, "cost_breakfast": {"text": ["How much did you pay for your breakfast?", "Wie viel haben Sie f\u00fcr ihr Fr\u00fchst\u00fcck bezahlt?"], "type": "float"}, "cost_dinner": {"text": ["How much did you pay for your dinner?", "Wie viel haben Sie f\u00fcr ihr Abendessen bezahlt?"], "type": "float"}, "cost_lunch": {"text": ["How much did you pay for your lunch?", "Wie viel haben Sie f\u00fcr ihr Mittagessen bezahlt?"], "type": "float"}, "duration": {"text": ["Survey duration", "Umfragedauer"], "type": "time"}, "endtime": {"text": ["Date of survey.", "Umfragedatum"], "type": "date"}, "full_name": {"name": "full_name", "text": ["What is your name?", "Wie hei\u00dfen Sie?"], "type": "string"}, "identity": {"name": "identity", "parent": {}, "properties": {"created": true}, "text": {"en-GB": "ID"}, "type": "int"}, "profile_gender": {"text": ["Gender", "Geschlecht"], "type": "single", "values": [{"text": ["Male", "M\u00e4nnlich"], "value": 1}, {"text": ["Female", "Weiblich"], "value": 2}]}, "q1_1": {"text": ["I like Coca-Cola.", "Ich mag Coca-Cola."], "type": "single", "values": ["lib@values@custom@12345 disagree-agree", "lib@values@core@97 dk"]}, "q1_2": {"text": ["I like Fanta.", "Ich mag Fanta."], "type": "single", "values": ["lib@values@custom@12345 disagree-agree", "lib@values@core@97 dk"]}, "q1_3": {"text": ["I like Sprite.", "Ich mag Sprite."], "type": "single", "values": ["lib@values@custom@12345 disagree-agree", "lib@values@core@97 dk"]}, "q1_4": {"text": ["I like 7-Up.", "Ich mag 7-Up."], "type": "single", "values": ["lib@values@custom@12345 disagree-agree", "lib@values@core@97 dk"]}, "q1_5": {"text": ["I like Dr Pepper.", "Ich mag Dr Pepper"], "type": "single", "values": ["lib@values@custom@12345 disagree-agree", "lib@values@core@97 dk"]}, "q2_1": {"text": ["Breakfast", "Fr\u00fchst\u00fcck"], "type": "single", "values": ["lib@values@custom@dichotomous"]}, "q2_2": {"text": ["Lunch", "Mittagessen"], "type": "single", "values": ["lib@values@custom@dichotomous"]}, "q2_3": {"text": ["Dinner", "Abendessen"], "type": "single", "values": ["lib@values@custom@dichotomous"]}, "q3_1": {"text": ["Meals eaten (1/3)", "Verzehrte Mahlzeiten (1/3)"], "type": "single", "values": ["lib@values@custom@meals"]}, "q3_2": {"text": ["Meals eaten (2/3)", "Verzehrte Mahlzeiten (2/3)"], "type": "single", "values": ["lib@values@custom@meals"]}, "q3_3": {"text": ["Meals eaten (3/3)", "Verzehrte Mahlzeiten (3/3)"], "type": "single", "values": ["lib@values@custom@meals"]}, "q4": {"sep": ";", "text": ["Which meals do you normally eat?", "Welche Mahlzeiten nehmen Sie normalerweise zu sich?"], "type": "delimited set", "values": ["lib@values@custom@meals"]}, "weight": {"name": "weight", "text": {"en-GB": "Weight"}, "type": "float"}}, "info": {"dimensions_comp": false, "dimensions_suffix": "_grid", "from_source": {"kwargs": {"header": 0, "index_col": 0, "infer_datetime_format": false, "parse_dates": true, "path": "./data/example_data_with_meta.csv", "sep": ",", "tupleize_cols": false}, "pandas_reader": "from_csv"}, "name": "ASDA tracker W4", "notes": "Client version at 2014/09/25"}, "lib": {"default text": "en-GB", "text": {"dk": ["Don't know", "weiss nicht"]}, "text map": {"auto": "enu", "map": ["enu", "deu"]}, "values": {"core": {"95 other": [{"text": ["Other", "Sonstige"], "value": 95}], "96 none": [{"text": ["None", "Keine"], "value": 96}], "96 none missing": [{"missing": true, "text": ["None", "Keine"], "value": 96}], "97 dk": [{"text": ["lib@text@dk"], "value": 97}], "97 dk missing": [{"missing": true, "text": ["lib@text@dk"], "value": 97}], "98 Not asked 99 Skipped": [{"missing": true, "text": ["Not asked", "Nicht gefragt"], "value": 98}, {"missing": true, "text": ["Skipped", "\u00dcbersprungen"], "value": 99}], "Not shown": [{"missing": true, "text": ["Not shown", "Nicht gesehen"], "value": 0}]}, "custom": {"12345 disagree-agree": [{"text": ["Disagree", "Stimme nicht zu"], "value": 1}, {"text": ["Somewhat disagree", "Stimme eher nicht zu"], "value": 2}, {"text": ["Neither", "Teils/teils"], "value": 3}, {"text": ["Somewhat agree", "Stimme eher zu"], "value": 4}, {"text": ["Agree", "Stimme zu"], "value": 5}], "dichotomous": [{"text": ["Selected", "Ja"], "value": 1}, {"text": ["Not selected", "Nein"], "value": 2}], "meals": [{"text": ["Breakfast", "Fr\u00fchst\u00fcck"], "value": 1}, {"text": ["Lunch", "Mittagessen"], "value": 2}, {"text": ["Dinner", "Abendessen"], "value": 3}]}}}, "masks": {"cost": {"items": [{"source": "columns@cost_breakfast", "text": ["Breakfast", "Fr\u00fchst\u00fcck"]}, {"source": "columns@cost_lunch", "text": ["Lunch", "Mittagessen"]}, {"source": "columns@cost_dinner", "text": ["Dinner", "Abendessen"]}], "text": ["How much did you pay for these meals?", "Wie viel haben Sie f\u00fcr diese Mahlzeiten bezahlt?"], "type": "array"}, "q1": {"items": [{"source": "columns@q1_1"}, {"source": "columns@q1_2"}, {"source": "columns@q1_3"}, {"source": "columns@q1_4"}, {"source": "columns@q1_5"}], "text": ["How much to you agree or disagree with the following statements?", "Inwieweit stimmen Sie den folgenden Aussagen zu?"], "type": "array"}, "q2": {"items": [{"source": "columns@q2_1"}, {"source": "columns@q2_2"}, {"source": "columns@q2_3"}], "text": ["Which meals do you normally eat?", "Welche Mahlzeiten nehmen Sie normalerweise zu sich?"], "type": "dichotomous set"}, "q3": {"items": [{"source": "columns@q3_1"}, {"source": "columns@q3_2"}, {"source": "columns@q3_3"}], "text": ["Which meals do you normally eat?", "Welche Mahlzeiten nehmen Sie normalerweise zu sich?"], "type": "categorical set"}}, "sets": {"client export": {"items": ["columns@profile_gender", "sets@survey variables"], "text": "Variables for external use"}, "data file": {"items": ["columns@identity", "columns@cost_breakfast", "columns@cost_lunch", "columns@cost_dinner", "columns@full_name", "columns@endtime", "columns@duration", "columns@profile_gender", "columns@age", "columns@age_group", "columns@q1_1", "columns@q1_2", "columns@q1_3", "columns@q1_4", "columns@q1_5", "columns@q2_1", "columns@q2_2", "columns@q2_3", "columns@q3_1", "columns@q3_2", "columns@q3_3", "columns@q4", "columns@weight"]}, "demographics": {"items": ["columns@full_name", "columns@profile_gender"], "text": "PDLs"}, "internal": {"items": ["sets@system", "columns@profile_gender", "sets@survey variables"], "text": "Variables for internal use"}, "survey variables": {"items": ["masks@cost", "masks@q1", "masks@q2", "masks@q3", "columns@q4"], "text": "Variables from questionnaire"}, "system": {"items": ["columns@identity", "columns@endtime"], "text": "System variables"}}, "type": "pandas.DataFrame"} \ No newline at end of file From 6a27f2340c288c6b80527003c8c11c0984d1abd2 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 21 May 2019 14:24:23 +0200 Subject: [PATCH 688/733] removal of not needed copy of engine dataset after dummy ceation --- quantipy/core/dataset.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 498caf3d2..9e7a67a62 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2888,13 +2888,7 @@ def weight(self, weight_scheme, weight_name='weight', unique_key='identity', meta, data = self.split() engine = qp.WeightEngine(data, meta=meta) engine.add_scheme(weight_scheme, key=unique_key, verbose=verbose) - - # =================================================== - eds = engine.dataset - if any('__logic_dummy__' in v for v in eds.singles()): - self._meta, self._data = eds.split() engine.run() - # =================================================== org_wname = weight_name if report: From 4996e54c09bac9aaf56bf22b79f1308f246568a5 Mon Sep 17 00:00:00 2001 From: Alexander Buchhammer Date: Tue, 21 May 2019 15:18:22 +0200 Subject: [PATCH 689/733] removing logical dummy variables leftovers --- quantipy/core/dataset.py | 9 +++++++++ quantipy/core/weights/rim.py | 3 +++ quantipy/core/weights/weight_engine.py | 25 ++++++++++++++----------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 9e7a67a62..b92355613 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -7060,6 +7060,15 @@ def derotate(self, levels, mapper, other=None, unique_key='identity', # DATA MANIPULATION/HANDLING # ------------------------------------------------------------------------ + def _logic_as_pd_expr(self, logic, prefix='default'): + """ + """ + varname = '{}__logic_dummy__'.format(prefix) + category = [(1, 'select', logic)] + meta = (varname, 'single', '', category) + self.derive(*meta) + return '{}==1'.format(varname) + def make_dummy(self, var, partitioned=False): if not self.is_array(var): vartype = self._get_type(var) diff --git a/quantipy/core/weights/rim.py b/quantipy/core/weights/rim.py index 57f18b542..964fffc8c 100644 --- a/quantipy/core/weights/rim.py +++ b/quantipy/core/weights/rim.py @@ -52,6 +52,7 @@ def __init__(self, # Constants self._FILTER_DEF = 'filters' + self._FILTER_DEF_ORG = 'filters_org' self._FILTER_VARS = 'filter_vars' self._TARGETS = 'targets' self._TARGETS_INDEX = 'targets_index' @@ -67,6 +68,7 @@ def __init__(self, self.groups[self._DEFAULT_NAME] = {} self.groups[self._DEFAULT_NAME][self._REPORT] = None self.groups[self._DEFAULT_NAME][self._FILTER_DEF] = None + self.groups[self._DEFAULT_NAME][self._FILTER_DEF_ORG] = None self.groups[self._DEFAULT_NAME][self._FILTER_VARS] = [] self.groups[self._DEFAULT_NAME][self._TARGETS] = self._empty_target_list() self.groups[self._DEFAULT_NAME][self._TARGETS_INDEX] = None @@ -146,6 +148,7 @@ def add_group(self, name=None, filter_def=None, targets=None): if targets is not None: self.set_targets(targets=targets, group_name=gn) self.groups[gn][self._FILTER_DEF] = filter_def + self.groups[gn][self._FILTER_DEF_ORG] = filter_def def _compute(self): self._get_base_factors() diff --git a/quantipy/core/weights/weight_engine.py b/quantipy/core/weights/weight_engine.py index 5e82a67db..fe933d6bc 100644 --- a/quantipy/core/weights/weight_engine.py +++ b/quantipy/core/weights/weight_engine.py @@ -204,10 +204,13 @@ def _resolve_filters(self, scheme): if f is not None: grps[grp]['filter_vars'] = self._find_filter_variables(f) if not isinstance(f, (str, unicode)): - f = self._replace_logical_filter(f, grp) + f = self.dataset._logic_as_pd_expr(f, grp) + filter_var = f.split('=')[0] + self._df[filter_var] = self.dataset._data[filter_var].copy() + self.dataset.drop(filter_var) msg = 'Converted {} filter to logical dummy expression: {}' print msg.format(grp, f) - grps[grp]['filter_vars'].append(f.split('=')[0]) + grps[grp]['filter_vars'].append(filter_var) grps[grp]['filters'] = f return None @@ -220,12 +223,12 @@ def _find_filter_variables(self, filter_expression): filter_variables.append(col) return filter_variables - def _replace_logical_filter(self, filter_expression, grp_name): - """ - """ - varname = '{}__logic_dummy__'.format(grp_name) - category = [(1, 'select', filter_expression)] - meta = (varname, 'single', '', category) - self.dataset.derive(*meta) - self._df[varname] = self.dataset._data[varname].copy() - return '{}==1'.format(varname) + # def _replace_logical_filter(self, filter_expression, grp_name): + # """ + # """ + # varname = '{}__logic_dummy__'.format(grp_name) + # category = [(1, 'select', filter_expression)] + # meta = (varname, 'single', '', category) + # self.dataset.derive(*meta) + # self._df[varname] = self.dataset._data[varname].copy() + # return '{}==1'.format(varname) From f71286faa3e2558eb163b647260357856f78ce04 Mon Sep 17 00:00:00 2001 From: "YG\\anders.freund" Date: Fri, 24 May 2019 11:38:44 +0200 Subject: [PATCH 690/733] add method add_chain() to ChainManager --- quantipy/sandbox/sandbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 35130ef58..247dc9c5b 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -105,6 +105,9 @@ def __next__(self): raise StopIteration next = __next__ + def add_chain(self, chain): + self.__chains.append(chain) + @property def folders(self): """ From 3f533fe8d9476d99d7610a3fd8ae39d16757bbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 24 May 2019 17:08:23 +0200 Subject: [PATCH 691/733] add private get_property method --- quantipy/core/dataset.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 8ff3269ca..4d705ab02 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -203,7 +203,7 @@ def _net_view_recodes(self): return self._by_property('recoded_net') def _by_property(self, prop): - return [v for v in self.variables() if self.get_property(v, prop)] + return [v for v in self.variables() if self._get_property(v, prop)] @verify(variables={'name': 'both'}) def missings(self, name=None): @@ -6398,14 +6398,20 @@ def set_factors(self, name, factormap, safe=False): def get_property(self, name, prop_name, text_key=None): """ """ - mask_ref = self._meta['masks'] - col_ref = self._meta['columns'] - if not text_key: text_key = self.text_key valid_props = ['base_text', 'created', 'recoded_net', 'recoded_stat', 'recoded_filter', '_no_valid_items', '_no_valid_values', 'simple_org_expr', 'level'] if prop_name not in valid_props: raise ValueError("'prop_name' must be one of {}".format(valid_props)) + return self._get_property(name, prop_name, text_key) + + @verify(variables={'name': 'both'}) + def _get_property(self, name, prop_name, text_key=None): + """ + """ + mask_ref = self._meta['masks'] + col_ref = self._meta['columns'] + if not text_key: text_key = self.text_key has_props = False if self.is_array(name): if 'properties' in mask_ref[name]: From beccdfbc4e8407c721874aa354c3cb2ba803e100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 31 May 2019 11:32:20 +0200 Subject: [PATCH 692/733] fix index in weight engine --- quantipy/core/weights/weight_engine.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/quantipy/core/weights/weight_engine.py b/quantipy/core/weights/weight_engine.py index fe933d6bc..0331a1a9f 100644 --- a/quantipy/core/weights/weight_engine.py +++ b/quantipy/core/weights/weight_engine.py @@ -193,11 +193,11 @@ def dataframe(self, scheme=None): def add_scheme(self, scheme, key, verbose=True): if scheme.name in self.schemes: print "Overwriting existing scheme '%s'." % scheme.name - self._resolve_filters(scheme) + self._resolve_filters(scheme, key) self.schemes[scheme.name] = {self._SCHEME: scheme, self._KEY: key} scheme._minimize_columns(self._df, key, verbose) - def _resolve_filters(self, scheme): + def _resolve_filters(self, scheme, key): grps = scheme.groups for grp in grps: f = grps[grp]['filters'] @@ -206,8 +206,20 @@ def _resolve_filters(self, scheme): if not isinstance(f, (str, unicode)): f = self.dataset._logic_as_pd_expr(f, grp) filter_var = f.split('=')[0] + + # make sure scheme df is sorted by key variable + self._df.set_index(key, inplace=True) + self._df.sort_index(inplace=True) + # make sure dataset is sorted by key variable + self.dataset._data.set_index(key, inplace=True) + self.dataset._data.sort_index(inplace=True) + # assign filter self._df[filter_var] = self.dataset._data[filter_var].copy() + # restore key as column + self._df.reset_index(inplace=True) + self.dataset._data.reset_index(inplace=True) self.dataset.drop(filter_var) + msg = 'Converted {} filter to logical dummy expression: {}' print msg.format(grp, f) grps[grp]['filter_vars'].append(filter_var) From c9aac84e22f91704a5b98691aee9a655c7561cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 4 Jun 2019 11:59:43 +0200 Subject: [PATCH 693/733] remove spaces from filter name --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 668dee6f1..77a9514f9 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -7069,7 +7069,7 @@ def derotate(self, levels, mapper, other=None, unique_key='identity', def _logic_as_pd_expr(self, logic, prefix='default'): """ """ - varname = '{}__logic_dummy__'.format(prefix) + varname = '{}__logic_dummy__'.format(prefix).replace(' ', '_') category = [(1, 'select', logic)] meta = (varname, 'single', '', category) self.derive(*meta) From bc78eeadaf75dea839a25da9e46f2298f2067084 Mon Sep 17 00:00:00 2001 From: Roxana Date: Thu, 6 Jun 2019 14:18:21 +0100 Subject: [PATCH 694/733] Fix for secured vars in DataSet.repair() --- quantipy/core/dataset.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 77a9514f9..e812d070c 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2387,6 +2387,27 @@ def restore_item_texts(self, arrays=None): self.set_item_texts(a, rename_items, tk, ed) return None + def _add_secure_variables(self): + """ Add variables in the CSV missing from the data-file set """ + actual = [] + for item in self._meta['sets']['data file']['items']: + key, name = item.split('@') + if key == 'columns': + actual.append(name) + elif key == 'masks': + for mitem in self._meta['masks'][name]['items']: + mkey, mname = mitem['source'].split('@') + actual.append(mname) + + expected = self._data.columns.values.tolist() + + for col in expected: + if col not in actual and col != '@1': + print('Adding {}'.format(col)) + items = self._meta['sets']['data file']['items'] + items.append('columns@{}'.format(col)) + return None + def repair(self): """ Try to fix legacy meta data inconsistencies and badly shaped array / @@ -2399,6 +2420,7 @@ def repair(self): self.restore_item_texts() self._clean_datafile_set() self._prevent_one_cat_set() + self._add_secure_variables() return None # ------------------------------------------------------------------------ From 22d9530bec606c0ee75ec08c756bcf7b1217e5ad Mon Sep 17 00:00:00 2001 From: jamesrkg Date: Tue, 11 Jun 2019 13:00:18 +0800 Subject: [PATCH 695/733] Guarding against newlines in the form of \r\n during dimensions text conversion --- quantipy/core/tools/dp/dimensions/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/tools/dp/dimensions/writer.py b/quantipy/core/tools/dp/dimensions/writer.py index 08a69b5da..2ff8ce798 100644 --- a/quantipy/core/tools/dp/dimensions/writer.py +++ b/quantipy/core/tools/dp/dimensions/writer.py @@ -399,7 +399,7 @@ def remove_newlines_in_string(string): """ """ s = string.copy() - s = s.apply(lambda x: str(x).replace('\n', '')) + s = s.apply(lambda x: str(x).replace('\r\n', '').replace('\n', '')) return s def convert_categorical(categorical): From 89a96e9cd487c96e9ed0c20a67e13cb7173d5fe2 Mon Sep 17 00:00:00 2001 From: jamesrkg Date: Tue, 18 Jun 2019 11:27:08 +0800 Subject: [PATCH 696/733] Added support for write_dimensions(..., reuse_mdd) --- quantipy/core/tools/dp/dimensions/writer.py | 16 +++++++++++----- quantipy/core/tools/dp/io.py | 9 ++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/quantipy/core/tools/dp/dimensions/writer.py b/quantipy/core/tools/dp/dimensions/writer.py index 2ff8ce798..3e3072178 100644 --- a/quantipy/core/tools/dp/dimensions/writer.py +++ b/quantipy/core/tools/dp/dimensions/writer.py @@ -427,7 +427,7 @@ def convert_categorical(categorical): return cat def dimensions_from_quantipy(meta, data, path_mdd, path_ddf, text_key=None, - CRLF="CR", run=True, clean_up=True): + CRLF="CR", run=True, clean_up=True, reuse_mdd=False): """ DESCP @@ -441,7 +441,10 @@ def dimensions_from_quantipy(meta, data, path_mdd, path_ddf, text_key=None, name = path_mdd.split('/')[-1].split('.')[0] path = '/'.join(path_mdd.split('/')[:-1]) if '/' in path_mdd: path = path + '/' - path_mrs = u'{}create_mdd [{}].mrs'.format(path, name) + if reuse_mdd: + path_mrs = u'{}create_mdd [{}] (MDD WAS REUSED).mrs'.format(path, name) + else: + path_mrs = u'{}create_mdd [{}].mrs'.format(path, name) path_dms = u'{}create_ddf [{}].dms'.format(path, name) path_paired_csv = u'{}{}_paired.csv'.format(path, name) path_datastore = u'{}{}_datastore.csv'.format(path, name) @@ -456,9 +459,12 @@ def dimensions_from_quantipy(meta, data, path_mdd, path_ddf, text_key=None, from subprocess import check_output, STDOUT, CalledProcessError try: print 'Converting to .ddf/.mdd...' - command = 'mrscriptcl "{}"'.format(path_mrs) - check_output(command, stderr=STDOUT, shell=True) - print '.mdd file generated successfully.' + if reuse_mdd: + print '.mdd file was reused: "{}"'.format(path_mdd) + else: + command = 'mrscriptcl "{}"'.format(path_mrs) + check_output(command, stderr=STDOUT, shell=True) + print '.mdd file generated successfully.' command = 'DMSRun "{}"'.format(path_dms) check_output(command, stderr=STDOUT, shell=True) print '.ddf file generated successfully.\n' diff --git a/quantipy/core/tools/dp/io.py b/quantipy/core/tools/dp/io.py index 6182f1b3b..4ef89af94 100644 --- a/quantipy/core/tools/dp/io.py +++ b/quantipy/core/tools/dp/io.py @@ -297,7 +297,8 @@ def read_dimensions(path_mdd, path_ddf): return meta, data def write_dimensions(meta, data, path_mdd, path_ddf, text_key=None, - CRLF="CR", run=True, clean_up=True): + CRLF="CR", run=True, clean_up=True, + reuse_mdd=False): default_stdout = sys.stdout default_stderr = sys.stderr @@ -306,8 +307,10 @@ def write_dimensions(meta, data, path_mdd, path_ddf, text_key=None, sys.stdout = default_stdout sys.stderr = default_stderr - out = dimensions_from_quantipy(meta, data, path_mdd, path_ddf, - text_key, CRLF, run, clean_up) + out = dimensions_from_quantipy( + meta, data, path_mdd, path_ddf, text_key, CRLF, run, + clean_up, reuse_mdd + ) default_stdout = sys.stdout default_stderr = sys.stderr From b8ac2613c75f9168e7ef5e507c54edd877b9e879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 19 Jun 2019 10:11:32 +0200 Subject: [PATCH 697/733] remove filter from manifest edits --- quantipy/core/batch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 5ff885fbb..1cd6bf9ee 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1265,9 +1265,7 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", if merge_f: ds.merge_filter(f, filters) - if not manifest_edits: - vlist.append(f) - if f and manifest_edits: + if f: ds.filter(self.name, {f: 0}, True) if merge_f: ds.drop(f) From c11fbc2205dc9fb897098d97bf751e6de797c579 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 28 Jun 2019 15:56:08 +0200 Subject: [PATCH 698/733] valid_tks updated by merging --- quantipy/core/dataset.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e812d070c..a7dab485f 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3604,6 +3604,12 @@ def hmerge(self, dataset, on=None, left_on=None, right_on=None, If the merge is not applied ``inplace``, a ``DataSet`` instance is returned. """ + new_tks = [] + for d in dataset: + for tk in d.valid_tks: + if not d in self.valid_tks and not d in new_tks: + new_tks.append(tk) + self.extend_valid_tks(new_tks) ds_left = (self._meta, self._data) ds_right = [(ds._meta, ds._data) for ds in dataset] if on is None and right_on in self.columns(): @@ -3625,6 +3631,7 @@ def hmerge(self, dataset, on=None, left_on=None, right_on=None, new_dataset._data = merged_data new_dataset._meta = merged_meta return new_dataset + return None def update(self, data, on='identity'): """ From bebfceeaff8b49daa98173784e15e903777eb686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Fri, 28 Jun 2019 16:43:32 +0200 Subject: [PATCH 699/733] improve dataset.transpose --- .pytest_cache/v/cache/nodeids | 50 ++++++++++---------- quantipy/core/dataset.py | 89 +++++++++++++++++------------------ 2 files changed, 68 insertions(+), 71 deletions(-) diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index 203477c9a..4ce4465ad 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -25,26 +25,26 @@ "tests/test_batch.py::TestBatch::test_slicing", "tests/test_batch.py::TestBatch::test_sorting", "tests/test_batch.py::TestBatch::test_transpose", - "tests/test_chain.py::TestChainConstructor::()::test_init", - "tests/test_chain.py::TestChainConstructor::()::test_str", - "tests/test_chain.py::TestChainConstructor::()::test_repr", - "tests/test_chain.py::TestChainConstructor::()::test_len", - "tests/test_chain.py::TestChainExceptions::()::test_get_non_existent_columns", - "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx0]", - "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx1]", - "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx2]", - "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx3]", - "tests/test_chain.py::TestChainGet::()::test_sig_transformation_simple", - "tests/test_chain.py::TestChainGet::()::test_annotations_fields", - "tests/test_chain.py::TestChainGet::()::test_annotations_populated", - "tests/test_chain.py::TestChainGet::()::test_annotations_list_append", - "tests/test_chain.py::TestChainGet::()::test_sig_transformation_large", - "tests/test_chain.py::TestChainUnnamedAdd::()::test_unnamed", - "tests/test_chain.py::TestChainAdd::()::test_named", - "tests/test_chain.py::TestChainAdd::()::test_str[params_structure0]", - "tests/test_chain.py::TestChainAdd::()::test_str[params_structure1]", - "tests/test_chain.py::TestChainAddRepaint::()::test_str[params_structure0]", - "tests/test_chain.py::TestChainAddRepaint::()::test_str[params_structure1]", + "tests/test_chain.py::TestChainConstructor::test_init", + "tests/test_chain.py::TestChainConstructor::test_str", + "tests/test_chain.py::TestChainConstructor::test_repr", + "tests/test_chain.py::TestChainConstructor::test_len", + "tests/test_chain.py::TestChainExceptions::test_get_non_existent_columns", + "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx0]", + "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx1]", + "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx2]", + "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx3]", + "tests/test_chain.py::TestChainGet::test_sig_transformation_simple", + "tests/test_chain.py::TestChainGet::test_annotations_fields", + "tests/test_chain.py::TestChainGet::test_annotations_populated", + "tests/test_chain.py::TestChainGet::test_annotations_list_append", + "tests/test_chain.py::TestChainGet::test_sig_transformation_large", + "tests/test_chain.py::TestChainUnnamedAdd::test_unnamed", + "tests/test_chain.py::TestChainAdd::test_named", + "tests/test_chain.py::TestChainAdd::test_str[params_structure0]", + "tests/test_chain.py::TestChainAdd::test_str[params_structure1]", + "tests/test_chain.py::TestChainAddRepaint::test_str[params_structure0]", + "tests/test_chain.py::TestChainAddRepaint::test_str[params_structure1]", "tests/test_chain_old.py::TestChainObject::test_auto_orientation", "tests/test_chain_old.py::TestChainObject::test_dervie_attributes", "tests/test_chain_old.py::TestChainObject::test_describe", @@ -131,11 +131,11 @@ "tests/test_dataset.py::TestDataSet::test_transpose", "tests/test_dataset.py::TestDataSet::test_uncode", "tests/test_dataset.py::TestDataSet::test_validate", - "tests/test_excel.py::TestExcel::()::test_structure[params0]", - "tests/test_excel.py::TestExcel::()::test_structure[params1]", - "tests/test_excel.py::TestExcel::()::test_structure[params2]", - "tests/test_excel.py::TestExcel::()::test_structure[params3]", - "tests/test_excel.py::TestExcel::()::test_structure[params4]", + "tests/test_excel.py::TestExcel::test_structure[params0]", + "tests/test_excel.py::TestExcel::test_structure[params1]", + "tests/test_excel.py::TestExcel::test_structure[params2]", + "tests/test_excel.py::TestExcel::test_structure[params3]", + "tests/test_excel.py::TestExcel::test_structure[params4]", "tests/test_io_dimensions.py::TestDimLabels::test_dimlabels", "tests/test_link.py::TestLinkObject::test_get_data", "tests/test_link.py::TestLinkObject::test_get_meta", diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index e812d070c..166842a80 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4333,7 +4333,8 @@ def unify_values(self, name, code_map, slicer=None, exclusive=False): @modify(to_list=['ignore_items', 'ignore_values']) @verify(variables={'name': 'masks'}, text_keys='text_key') def transpose(self, name, new_name=None, ignore_items=None, - ignore_values=None, copy_data=True, text_key=None): + ignore_values=None, copy_data=True, text_key=None, + overwrite=False): """ Create a new array mask with transposed items / values structure. @@ -4360,58 +4361,54 @@ def transpose(self, name, new_name=None, ignore_items=None, text_key : str The text key to be used when generating text objects, i.e. item and value labels. + overwrite: bool, default False + Overwrite variable if `new_name` is already included. Returns ------- None DataSet is modified inplace. """ - org_name = name - # Get array item and value structure - reg_items_object = self._get_itemmap(name) - if ignore_items: - reg_items_object = [i for idx, i in - enumerate(reg_items_object, start=1) - if idx not in ignore_items] - reg_item_names = [item[0] for item in reg_items_object] - reg_item_texts = [item[1] for item in reg_items_object] - - reg_value_object = self._get_valuemap(name) - if ignore_values: - reg_value_object = [v for v in reg_value_object if v[0] - not in ignore_values] - reg_val_codes = [v[0] for v in reg_value_object] - reg_val_texts = [v[1] for v in reg_value_object] - - # Transpose the array structure: values --> items, items --> values - trans_items = [(code, value) for code, value in - zip(reg_val_codes, reg_val_texts)] - trans_values = [(idx, text) for idx, text in - enumerate(reg_item_texts, start=1)] - label = self.text(name, False, text_key) - # Create the new meta data entry for the transposed array structure + if (new_name and self.self._dims_compat_arr_name(new_name) in self and + not overwrite): + raise ValueError("'{}' is already included.".format(new_name)) if not new_name: - new_name = '{}_trans'.format(self._dims_free_arr_name(name)) - qtype = 'delimited set' - self.add_meta(new_name, qtype, label, trans_values, trans_items, text_key) - # Do the case data transformation by looping through items and - # convertig value code entries... - new_name = self._dims_compat_arr_name(new_name) - trans_items = self._get_itemmap(new_name, 'items') - trans_values = self._get_valuemap(new_name, 'codes') - for reg_item_name, new_val_code in zip(reg_item_names, trans_values): - for reg_val_code, trans_item in zip(reg_val_codes, trans_items): - if trans_item not in self._data.columns: - if qtype == 'delimited set': - self[trans_item] = '' - else: - self[trans_item] = np.NaN - if copy_data: - slicer = {reg_item_name: [reg_val_code]} - self.recode(trans_item, {new_val_code: slicer}, - append=True) - if self._verbose_infos: - print 'Transposed array: {} into {}'.format(org_name, new_name) + new_name = '{}_trans'.format(name) + if new_name == name: + tname = '{}_trans'.format(new_name) + else: + tname = new_name + + # input item_map + item_map = self._get_itemmap(name) + item_labels = [(x, i[1]) for x, i in enumerate(item_map, 1) + if not x in ignore_items] + item_vars = [(x, i[0]) for x, i in enumerate(item_map, 1) + if not x in ignore_items] + # input value_map + value_map = self._get_valuemap(name) + value_map = [v for x, v in enumerate(value_map, 1) + if not x in ignore_values] + # input label + label = self.text(name, False, text_key) + # add transposed meta + self.add_meta(tname, "delimited set", label, item_labels, value_map, text_key) + tname = self._dims_compat_arr_name(tname) + + # transpose the data + tsources = self.sources(tname) + mapper = { + tsource: {} for tsource in tsources + } + for code, source in zip([val[0] for val in value_map], tsources): + mapper = {} + for x, item in item_vars: + mapper[x] = {item: code} + self.recode(source, mapper, append=True) + if new_name == name: + print("Overwrite '{}'.".format(name)) + self.drop(name) + self.rename(tname, new_name) @verify(variables={'target': 'columns'}) def recode(self, target, mapper, default=None, append=False, From 4ada9b8f9a68e0907c27c85c3b6c88f2ad602bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 2 Jul 2019 11:07:09 +0200 Subject: [PATCH 700/733] check replacements' --- quantipy/core/batch.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 1cd6bf9ee..c5db6211e 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -855,7 +855,7 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, dupes = [v for v in oe if v in break_by] if dupes: raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) - def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): + def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite, repl): if filter_by: f_name = title if not self.filter else '%s_%s' % (self.filter, title) f_name = self._verify_filter_name(f_name, number=True) @@ -877,7 +877,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): 'break_by': break_by, 'incl_nan': incl_nan, 'drop_empty': drop_empty, - 'replace': replacements} + 'replace': repl} if any(o['title'] == title for o in self.verbatims): for x, o in enumerate(self.verbatims): if o['title'] == title: @@ -885,6 +885,21 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): else: self.verbatims.append(oe) + def _check_replacements(repl): + if not repl: + return None + elif not isinstance(repl, dict): + raise ValueError("replacements must be a dict.") + else: + if all(isinstance(v, dict) for v in repl.values()): + if self.language not in repl: + raise KeyError("batch.language is not included in replacements.") + else: + return repl[self.language] + else: + return repl + + repl = _check_replacements(replacements) if len(oe) + len(break_by) == 0: raise ValueError("Please add any variables as 'oe' or 'break_by'.") if split: @@ -893,9 +908,9 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): raise ValueError(msg) for t, open_end in zip(title, oe): open_end = [open_end] - _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite) + _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite, repl) else: - _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite) + _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite, repl) self._update() return None From 6031f227292abf759d1d1d1448e8e825a930e48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 3 Jul 2019 11:17:03 +0200 Subject: [PATCH 701/733] skip break_by and filter for oes --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index c5db6211e..21e32fb04 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1348,7 +1348,7 @@ def _get_vlist(self, batch, mode): if key == "oe": oes = [] for oe in var[:]: - oes += oe["break_by"] + oe["columns"] + [oe["filter"]] + oes += oe["columns"] var = oes if key == "f": var = batch["filter_names"] + batch["y_filter_map"].values() From e5c8b732476cd550eb9a02c39322a80c44bd3264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 3 Jul 2019 16:17:31 +0200 Subject: [PATCH 702/733] typo --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index ea700f132..78b82e111 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4376,7 +4376,7 @@ def transpose(self, name, new_name=None, ignore_items=None, None DataSet is modified inplace. """ - if (new_name and self.self._dims_compat_arr_name(new_name) in self and + if (new_name and self._dims_compat_arr_name(new_name) in self and not overwrite): raise ValueError("'{}' is already included.".format(new_name)) if not new_name: From 2d38ac558d980dedc5927a1cd0c17c4456fdfea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 9 Jul 2019 13:12:55 +0200 Subject: [PATCH 703/733] bugfix for i1257 --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 21e32fb04..00386a9ad 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1007,7 +1007,7 @@ def extend_filter(self, ext_filters): self.extend_filter_var(self.filter, log, v) else: f_name = '{}_f'.format(v) - self.add_filter_var(f_name, log) + self.add_filter_var(f_name, logic) self.extended_filters_per_x.update({v: f_name}) self._update() return None From addabeba9ed53aff883635944b90fbfef6e6a593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 9 Jul 2019 13:49:50 +0200 Subject: [PATCH 704/733] feature i1245 --- quantipy/core/dataset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 78b82e111..7f44c5d9a 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3775,7 +3775,8 @@ def duplicates(self, name='identity'): vals = [int(i) for i in vals] return vals - def drop_duplicates(self, unique_id='identity', keep='first'): + @verify(variables={'sort_by': 'columns'}) + def drop_duplicates(self, unique_id='identity', keep='first', sort_by=None): """ Drop duplicated cases from self._data. @@ -3785,7 +3786,13 @@ def drop_duplicates(self, unique_id='identity', keep='first'): Variable name that gets scanned for duplicates. keep : str, {'first', 'last'} Keep first or last of the duplicates. + sort_by : str + Name of a variable to sort the data by, for example "endtime". + It is a helper to specify `keep`. """ + if sort_by: + self._data.sort(sort_by, inplace=True) + self._data.reset_index(inplace=True) if self.duplicates(unique_id): cases_before = self._data.shape[0] self._data.drop_duplicates(subset=unique_id, keep=keep, inplace=True) From 6015f97aa626880130802e8a9ace50d4be0b514b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 11 Jul 2019 13:28:35 +0200 Subject: [PATCH 705/733] fix filter for oe in batch.clone --- quantipy/core/batch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 00386a9ad..b1a0f7993 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -244,10 +244,7 @@ def clone(self, name, b_filter=None, as_addition=False): batch_copy.add_filter(b_filter[0], b_filter[1]) if batch_copy.verbatims and b_filter and not as_addition: for oe in batch_copy.verbatims: - data = self._data.copy() - series_data = data['@1'].copy()[pd.Index(oe['idx'])] - slicer, _ = get_logic_index(series_data, b_filter[1], data) - oe['idx'] = slicer.tolist() + oe["filter"] = b_filter[0] if as_addition: batch_copy.as_addition(self.name) batch_copy._update() From 1c56f79bb165851dd97cbce2b836c786c654f3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 11 Jul 2019 14:08:38 +0200 Subject: [PATCH 706/733] fix filter for oe in batch.clone --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index b1a0f7993..2ea9926e6 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -244,7 +244,7 @@ def clone(self, name, b_filter=None, as_addition=False): batch_copy.add_filter(b_filter[0], b_filter[1]) if batch_copy.verbatims and b_filter and not as_addition: for oe in batch_copy.verbatims: - oe["filter"] = b_filter[0] + oe["filter"] = batch_copy.filter if as_addition: batch_copy.as_addition(self.name) batch_copy._update() From 2e7c36b18030582de27c70eb2906b4897814b1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Jul 2019 08:52:57 +0200 Subject: [PATCH 707/733] handle filters in Batch.to_dataset --- .pytest_cache/v/cache/nodeids | 50 +++++++++++++++++------------------ quantipy/core/batch.py | 27 +++++++------------ quantipy/core/dataset.py | 12 +++++++++ 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids index 4ce4465ad..203477c9a 100644 --- a/.pytest_cache/v/cache/nodeids +++ b/.pytest_cache/v/cache/nodeids @@ -25,26 +25,26 @@ "tests/test_batch.py::TestBatch::test_slicing", "tests/test_batch.py::TestBatch::test_sorting", "tests/test_batch.py::TestBatch::test_transpose", - "tests/test_chain.py::TestChainConstructor::test_init", - "tests/test_chain.py::TestChainConstructor::test_str", - "tests/test_chain.py::TestChainConstructor::test_repr", - "tests/test_chain.py::TestChainConstructor::test_len", - "tests/test_chain.py::TestChainExceptions::test_get_non_existent_columns", - "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx0]", - "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx1]", - "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx2]", - "tests/test_chain.py::TestChainGet::test_get_x_orientation[params_getx3]", - "tests/test_chain.py::TestChainGet::test_sig_transformation_simple", - "tests/test_chain.py::TestChainGet::test_annotations_fields", - "tests/test_chain.py::TestChainGet::test_annotations_populated", - "tests/test_chain.py::TestChainGet::test_annotations_list_append", - "tests/test_chain.py::TestChainGet::test_sig_transformation_large", - "tests/test_chain.py::TestChainUnnamedAdd::test_unnamed", - "tests/test_chain.py::TestChainAdd::test_named", - "tests/test_chain.py::TestChainAdd::test_str[params_structure0]", - "tests/test_chain.py::TestChainAdd::test_str[params_structure1]", - "tests/test_chain.py::TestChainAddRepaint::test_str[params_structure0]", - "tests/test_chain.py::TestChainAddRepaint::test_str[params_structure1]", + "tests/test_chain.py::TestChainConstructor::()::test_init", + "tests/test_chain.py::TestChainConstructor::()::test_str", + "tests/test_chain.py::TestChainConstructor::()::test_repr", + "tests/test_chain.py::TestChainConstructor::()::test_len", + "tests/test_chain.py::TestChainExceptions::()::test_get_non_existent_columns", + "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx0]", + "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx1]", + "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx2]", + "tests/test_chain.py::TestChainGet::()::test_get_x_orientation[params_getx3]", + "tests/test_chain.py::TestChainGet::()::test_sig_transformation_simple", + "tests/test_chain.py::TestChainGet::()::test_annotations_fields", + "tests/test_chain.py::TestChainGet::()::test_annotations_populated", + "tests/test_chain.py::TestChainGet::()::test_annotations_list_append", + "tests/test_chain.py::TestChainGet::()::test_sig_transformation_large", + "tests/test_chain.py::TestChainUnnamedAdd::()::test_unnamed", + "tests/test_chain.py::TestChainAdd::()::test_named", + "tests/test_chain.py::TestChainAdd::()::test_str[params_structure0]", + "tests/test_chain.py::TestChainAdd::()::test_str[params_structure1]", + "tests/test_chain.py::TestChainAddRepaint::()::test_str[params_structure0]", + "tests/test_chain.py::TestChainAddRepaint::()::test_str[params_structure1]", "tests/test_chain_old.py::TestChainObject::test_auto_orientation", "tests/test_chain_old.py::TestChainObject::test_dervie_attributes", "tests/test_chain_old.py::TestChainObject::test_describe", @@ -131,11 +131,11 @@ "tests/test_dataset.py::TestDataSet::test_transpose", "tests/test_dataset.py::TestDataSet::test_uncode", "tests/test_dataset.py::TestDataSet::test_validate", - "tests/test_excel.py::TestExcel::test_structure[params0]", - "tests/test_excel.py::TestExcel::test_structure[params1]", - "tests/test_excel.py::TestExcel::test_structure[params2]", - "tests/test_excel.py::TestExcel::test_structure[params3]", - "tests/test_excel.py::TestExcel::test_structure[params4]", + "tests/test_excel.py::TestExcel::()::test_structure[params0]", + "tests/test_excel.py::TestExcel::()::test_structure[params1]", + "tests/test_excel.py::TestExcel::()::test_structure[params2]", + "tests/test_excel.py::TestExcel::()::test_structure[params3]", + "tests/test_excel.py::TestExcel::()::test_structure[params4]", "tests/test_io_dimensions.py::TestDimLabels::test_dimlabels", "tests/test_link.py::TestLinkObject::test_get_data", "tests/test_link.py::TestLinkObject::test_get_meta", diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 2ea9926e6..e565a238b 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1253,20 +1253,6 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", vlist = self.de_duplicate(vlist) vlist = self.roll_up(vlist) - # handle filters - merge_f = False - f = self.filter - if adds: - filters = [self.filter] + [batches[add]['filter'] for add in adds] - filters = [fi for fi in filters if fi] - if len(filters) == 1: - f = filters[0] - elif not self.compare_filter(filters[0], filters[1:]): - f = "merge_filter" - merge_f = filters - else: - f = filters[0] - # create ds ds = qp.DataSet(self.name, self._dimensions_comp) ds.from_components(self._data.copy(), org_copy.deepcopy(self._meta), @@ -1275,11 +1261,18 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", if not (b in adds or b == ds.name): del ds._meta['sets']['batches'][b] - if merge_f: - ds.merge_filter(f, filters) + # handle filters + f = self.filter + filters = list(set([ + batches[a]['filter'] for a in adds if batches[a]['filter']])) + if filters: + if not self.is_subfilter(f, filters): + f = "merge_filter" + filters.append(self.filter) + ds.merge_filter(f, filters) if f: ds.filter(self.name, {f: 0}, True) - if merge_f: + if f == "merge_filter": ds.drop(f) ds.create_set(str(self.name), included=vlist, overwrite=True) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 7f44c5d9a..5ce087afc 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3555,6 +3555,18 @@ def compare_filter(self, name1, name2): equal = False return equal + @modify(to_list=["name2"]) + def is_subfilter(self, name1, name2): + """ + Verify if index of name2 is part of the index of name1. + """ + idx = self.manifest_filter(name1).tolist() + included = True + for n in name2: + if [i for i in self.manifest_filter(n).tolist() if i not in idx]: + included = False + return included + # ------------------------------------------------------------------------ # extending / merging # ------------------------------------------------------------------------ From f4894dc4b0b82106ccb77bde6dfe231347915b4b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 23 Jul 2019 09:41:02 +0200 Subject: [PATCH 708/733] oe filter update --- quantipy/core/batch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index e565a238b..093bc0853 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1243,6 +1243,7 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", vlist += self._get_vlist(batches[add], mode) if not from_set: from_set = vlist + vlist = self.align_order(vlist, from_set, integrate_rc, fix=misc) if additions == "sort_within": for add in adds: @@ -1338,7 +1339,14 @@ def _get_vlist(self, batch, mode): if key == "oe": oes = [] for oe in var[:]: +<<<<<<< Updated upstream oes += oe["columns"] +======= + if 'f' in mode: + oes += oe["break_by"] + oe["columns"] + [oe["filter"]] + else: + oes += oe['columns'] +>>>>>>> Stashed changes var = oes if key == "f": var = batch["filter_names"] + batch["y_filter_map"].values() From 3ae5e44062e62abe794c55c3cc59982ae9402111 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 23 Jul 2019 09:50:59 +0200 Subject: [PATCH 709/733] add_y_on_y fix for duped names with diff filters --- quantipy/core/batch.py | 69 ++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 093bc0853..58199f8a7 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -244,7 +244,10 @@ def clone(self, name, b_filter=None, as_addition=False): batch_copy.add_filter(b_filter[0], b_filter[1]) if batch_copy.verbatims and b_filter and not as_addition: for oe in batch_copy.verbatims: - oe["filter"] = batch_copy.filter + data = self._data.copy() + series_data = data['@1'].copy()[pd.Index(oe['idx'])] + slicer, _ = get_logic_index(series_data, b_filter[1], data) + oe['idx'] = slicer.tolist() if as_addition: batch_copy.as_addition(self.name) batch_copy._update() @@ -852,7 +855,7 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, dupes = [v for v in oe if v in break_by] if dupes: raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) - def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite, repl): + def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): if filter_by: f_name = title if not self.filter else '%s_%s' % (self.filter, title) f_name = self._verify_filter_name(f_name, number=True) @@ -874,7 +877,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite, rep 'break_by': break_by, 'incl_nan': incl_nan, 'drop_empty': drop_empty, - 'replace': repl} + 'replace': replacements} if any(o['title'] == title for o in self.verbatims): for x, o in enumerate(self.verbatims): if o['title'] == title: @@ -882,21 +885,6 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite, rep else: self.verbatims.append(oe) - def _check_replacements(repl): - if not repl: - return None - elif not isinstance(repl, dict): - raise ValueError("replacements must be a dict.") - else: - if all(isinstance(v, dict) for v in repl.values()): - if self.language not in repl: - raise KeyError("batch.language is not included in replacements.") - else: - return repl[self.language] - else: - return repl - - repl = _check_replacements(replacements) if len(oe) + len(break_by) == 0: raise ValueError("Please add any variables as 'oe' or 'break_by'.") if split: @@ -905,9 +893,9 @@ def _check_replacements(repl): raise ValueError(msg) for t, open_end in zip(title, oe): open_end = [open_end] - _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite, repl) + _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite) else: - _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite, repl) + _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite) self._update() return None @@ -1004,7 +992,7 @@ def extend_filter(self, ext_filters): self.extend_filter_var(self.filter, log, v) else: f_name = '{}_f'.format(v) - self.add_filter_var(f_name, logic) + self.add_filter_var(f_name, log) self.extended_filters_per_x.update({v: f_name}) self._update() return None @@ -1045,6 +1033,8 @@ def add_y_on_y(self, name, y_filter=None, main_filter='extend'): else: main_filter = 'replace' self.y_on_y_filter[name] = (main_filter, y_filter) + if name in self.y_filter_map: + del self.y_filter_map[name] self._update() return None @@ -1243,7 +1233,6 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", vlist += self._get_vlist(batches[add], mode) if not from_set: from_set = vlist - vlist = self.align_order(vlist, from_set, integrate_rc, fix=misc) if additions == "sort_within": for add in adds: @@ -1254,6 +1243,20 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", vlist = self.de_duplicate(vlist) vlist = self.roll_up(vlist) + # handle filters + merge_f = False + f = self.filter + if adds: + filters = [self.filter] + [batches[add]['filter'] for add in adds] + filters = [fi for fi in filters if fi] + if len(filters) == 1: + f = filters[0] + elif not self.compare_filter(filters[0], filters[1:]): + f = "merge_filter" + merge_f = filters + else: + f = filters[0] + # create ds ds = qp.DataSet(self.name, self._dimensions_comp) ds.from_components(self._data.copy(), org_copy.deepcopy(self._meta), @@ -1262,18 +1265,11 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", if not (b in adds or b == ds.name): del ds._meta['sets']['batches'][b] - # handle filters - f = self.filter - filters = list(set([ - batches[a]['filter'] for a in adds if batches[a]['filter']])) - if filters: - if not self.is_subfilter(f, filters): - f = "merge_filter" - filters.append(self.filter) - ds.merge_filter(f, filters) + if merge_f: + ds.merge_filter(f, filters) if f: ds.filter(self.name, {f: 0}, True) - if f == "merge_filter": + if merge_f: ds.drop(f) ds.create_set(str(self.name), included=vlist, overwrite=True) @@ -1339,14 +1335,7 @@ def _get_vlist(self, batch, mode): if key == "oe": oes = [] for oe in var[:]: -<<<<<<< Updated upstream - oes += oe["columns"] -======= - if 'f' in mode: - oes += oe["break_by"] + oe["columns"] + [oe["filter"]] - else: - oes += oe['columns'] ->>>>>>> Stashed changes + oes += oe["break_by"] + oe["columns"] + [oe["filter"]] var = oes if key == "f": var = batch["filter_names"] + batch["y_filter_map"].values() From 44eb8364ce529b7dfb96eeffd302942e5765149f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Jul 2019 10:05:17 +0200 Subject: [PATCH 710/733] fix conflict --- quantipy/core/batch.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 093bc0853..484787506 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1339,14 +1339,10 @@ def _get_vlist(self, batch, mode): if key == "oe": oes = [] for oe in var[:]: -<<<<<<< Updated upstream - oes += oe["columns"] -======= if 'f' in mode: - oes += oe["break_by"] + oe["columns"] + [oe["filter"]] + oes += oe["columns"] + [oe["filter"]] else: oes += oe['columns'] ->>>>>>> Stashed changes var = oes if key == "f": var = batch["filter_names"] + batch["y_filter_map"].values() From 3f59c3b6c47f3e2c8d92f252dc1377ce37146308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 23 Jul 2019 10:12:47 +0200 Subject: [PATCH 711/733] bring back old commits --- quantipy/core/batch.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 446dd10b4..366c8e123 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -244,10 +244,7 @@ def clone(self, name, b_filter=None, as_addition=False): batch_copy.add_filter(b_filter[0], b_filter[1]) if batch_copy.verbatims and b_filter and not as_addition: for oe in batch_copy.verbatims: - data = self._data.copy() - series_data = data['@1'].copy()[pd.Index(oe['idx'])] - slicer, _ = get_logic_index(series_data, b_filter[1], data) - oe['idx'] = slicer.tolist() + oe["filter"] = batch_copy.filter if as_addition: batch_copy.as_addition(self.name) batch_copy._update() @@ -855,7 +852,7 @@ def add_open_ends(self, oe, break_by=None, drop_empty=True, incl_nan=False, dupes = [v for v in oe if v in break_by] if dupes: raise ValueError("'{}' included in oe and break_by.".format("', '".join(dupes))) - def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): + def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite, repl): if filter_by: f_name = title if not self.filter else '%s_%s' % (self.filter, title) f_name = self._verify_filter_name(f_name, number=True) @@ -877,7 +874,7 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): 'break_by': break_by, 'incl_nan': incl_nan, 'drop_empty': drop_empty, - 'replace': replacements} + 'replace': repl} if any(o['title'] == title for o in self.verbatims): for x, o in enumerate(self.verbatims): if o['title'] == title: @@ -885,6 +882,22 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): else: self.verbatims.append(oe) + def _check_replacements(repl): + if not repl: + return None + elif not isinstance(repl, dict): + raise ValueError("replacements must be a dict.") + else: + if all(isinstance(v, dict) for v in repl.values()): + if self.language not in repl: + raise KeyError("batch.language is not included in replacements.") + else: + return repl[self.language] + else: + return repl + + repl = _check_replacements(replacements) + if len(oe) + len(break_by) == 0: raise ValueError("Please add any variables as 'oe' or 'break_by'.") if split: @@ -893,9 +906,9 @@ def _add_oe(oe, break_by, title, drop_empty, incl_nan, filter_by, overwrite): raise ValueError(msg) for t, open_end in zip(title, oe): open_end = [open_end] - _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite) + _add_oe(open_end, break_by, t, drop_empty, incl_nan, filter_by, overwrite, repl) else: - _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite) + _add_oe(oe, break_by, title[0], drop_empty, incl_nan, filter_by, overwrite, repl) self._update() return None @@ -992,7 +1005,7 @@ def extend_filter(self, ext_filters): self.extend_filter_var(self.filter, log, v) else: f_name = '{}_f'.format(v) - self.add_filter_var(f_name, log) + self.add_filter_var(f_name, logic) self.extended_filters_per_x.update({v: f_name}) self._update() return None @@ -1335,7 +1348,7 @@ def _get_vlist(self, batch, mode): if key == "oe": oes = [] for oe in var[:]: - if 'f' in mode: + if 'f' in mode: oes += oe["columns"] + [oe["filter"]] else: oes += oe['columns'] From 493061eabac82f8d31160b29c7d4341e119d6480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 31 Jul 2019 08:51:52 +0200 Subject: [PATCH 712/733] fix drop_duplicates --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 5ce087afc..17111dfed 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -3804,7 +3804,7 @@ def drop_duplicates(self, unique_id='identity', keep='first', sort_by=None): """ if sort_by: self._data.sort(sort_by, inplace=True) - self._data.reset_index(inplace=True) + self._data.reset_index(drop=True, inplace=True) if self.duplicates(unique_id): cases_before = self._data.shape[0] self._data.drop_duplicates(subset=unique_id, keep=keep, inplace=True) From cc3822fae35baf3f737200c85bdca6b39a7cf568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 31 Jul 2019 08:59:03 +0200 Subject: [PATCH 713/733] extend to_dataset features --- quantipy/core/batch.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 366c8e123..4fd8414d8 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1331,6 +1331,9 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", ds._meta['masks'][v].pop('rules') else: ds._meta['columns'][v].pop('rules') + self.set_text_key(self.language) + if "oe" in mode: + self._apply_oe_replacements(ds) return ds def _get_vlist(self, batch, mode): @@ -1360,3 +1363,17 @@ def _get_vlist(self, batch, mode): if v and v in self and v not in vlist: vlist.append(v) return vlist + + def _apply_oe_replacements(self, dataset): + for oe in self.verbatims: + if oe['replace']: + for target, repl in oe['replace'].items(): + if not repl: + repl = np.NaN + dataset._data.replace(target, repl, inplace=True) + if oe['drop_empty']: + dataset._data.dropna( + subset=oe['columns'], how='all', inplace=True) + if not oe['incl_nan']: + for col in oe['columns']: + dataset._data[col].replace(np.NaN, '', inplace=True) From dd1f0bb2474e6ed24bd0c88bd7487037dd0b47eb Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 1 Aug 2019 17:01:37 +0200 Subject: [PATCH 714/733] all bugfix for rules application Signed-off-by: Your Name --- quantipy/core/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/rules.py b/quantipy/core/rules.py index f56e705b3..1a64433ea 100644 --- a/quantipy/core/rules.py +++ b/quantipy/core/rules.py @@ -623,7 +623,7 @@ def verify_test_value(value): value = set(value) else: value = set([int(i) if i.isdigit() else i - for i in list(value[1:-1].split(','))]) + for i in list(value[1:-1].split(', '))]) value = cols.intersection(value) if not value: value = '' From 398bed40a1a8d82065410c8158107e8c7497ea30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Mon, 5 Aug 2019 07:02:24 +0200 Subject: [PATCH 715/733] bugfix in to_dataset --- quantipy/core/batch.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 4fd8414d8..d3e0e523f 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1371,9 +1371,6 @@ def _apply_oe_replacements(self, dataset): if not repl: repl = np.NaN dataset._data.replace(target, repl, inplace=True) - if oe['drop_empty']: - dataset._data.dropna( - subset=oe['columns'], how='all', inplace=True) if not oe['incl_nan']: for col in oe['columns']: dataset._data[col].replace(np.NaN, '', inplace=True) From 8b647b24e24b886fce24774c1703b3aee4e2f8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 6 Aug 2019 10:05:48 +0200 Subject: [PATCH 716/733] skip systemvariables like @1 or id_L1 in the repair --- quantipy/core/dataset.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 17111dfed..24af242bf 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -2389,9 +2389,12 @@ def restore_item_texts(self, arrays=None): def _add_secure_variables(self): """ Add variables in the CSV missing from the data-file set """ + ignore = ['@1', "id_L1"] actual = [] for item in self._meta['sets']['data file']['items']: key, name = item.split('@') + if name in ignore: + continue if key == 'columns': actual.append(name) elif key == 'masks': @@ -2402,7 +2405,9 @@ def _add_secure_variables(self): expected = self._data.columns.values.tolist() for col in expected: - if col not in actual and col != '@1': + if col in ignore: + continue + if col not in actual: print('Adding {}'.format(col)) items = self._meta['sets']['data file']['items'] items.append('columns@{}'.format(col)) From f49f6e6d8112259beb39f98a2c26bd33d8ea6a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 7 Aug 2019 14:21:16 +0200 Subject: [PATCH 717/733] add properties key to columns --- quantipy/core/tools/dp/ascribe/reader.py | 6 ++++-- quantipy/core/tools/dp/spss/reader.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/quantipy/core/tools/dp/ascribe/reader.py b/quantipy/core/tools/dp/ascribe/reader.py index 834366d2b..e5e756d5c 100644 --- a/quantipy/core/tools/dp/ascribe/reader.py +++ b/quantipy/core/tools/dp/ascribe/reader.py @@ -24,7 +24,8 @@ def quantipy_from_ascribe(path_xml, path_txt, text_key='main'): 'name': 'responseid', 'type': 'int', 'text': {text_key: 'responseid'}, - 'parent': {} + 'parent': {}, + 'properties': {} } meta['sets']['data file']['items'] = ['columns@responseid'] @@ -86,7 +87,8 @@ def quantipy_from_ascribe(path_xml, path_txt, text_key='main'): 'type': col_type, 'text': var_text, 'values': values, - 'parent': {} + 'parent': {}, + 'properties': {} } # Add the newly defined column to the Quantipy meta diff --git a/quantipy/core/tools/dp/spss/reader.py b/quantipy/core/tools/dp/spss/reader.py index cd3e91aca..2887597af 100644 --- a/quantipy/core/tools/dp/spss/reader.py +++ b/quantipy/core/tools/dp/spss/reader.py @@ -203,6 +203,7 @@ def extract_sav_meta(sav_file, name="", data=None, ioLocale='en_US.UTF-8', 'name': mrset, 'type': 'delimited set', 'text': {text_key: metadata.multRespDefs[mrset]['label']}, + 'properties': {}, 'parent': {}, 'values': values} # Add the new delimited set to the 'data file' set From c0930d7317e690c66fef5a924d5b0a43a50261c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 7 Aug 2019 15:09:14 +0200 Subject: [PATCH 718/733] add missing property key in SPSS conversion --- quantipy/core/tools/dp/spss/reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quantipy/core/tools/dp/spss/reader.py b/quantipy/core/tools/dp/spss/reader.py index 2887597af..be4c2e3d3 100644 --- a/quantipy/core/tools/dp/spss/reader.py +++ b/quantipy/core/tools/dp/spss/reader.py @@ -89,6 +89,7 @@ def extract_sav_meta(sav_file, name="", data=None, ioLocale='en_US.UTF-8', meta['columns'][column] = {} meta['columns'][column]['name'] = column meta['columns'][column]['parent'] = {} + meta['columns'][column]['properties'] = {} if column in metadata.valueLabels: # ValueLabels is type = 'single' (possibry 1-1 map) meta['columns'][column]['values'] = [] From 73c50d623814529a81d61e01973588801d79d945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Wed, 7 Aug 2019 15:46:07 +0200 Subject: [PATCH 719/733] take over painted index in assign and check for viewlike in _describe_block --- quantipy/sandbox/sandbox.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/quantipy/sandbox/sandbox.py b/quantipy/sandbox/sandbox.py index 247dc9c5b..72114274a 100644 --- a/quantipy/sandbox/sandbox.py +++ b/quantipy/sandbox/sandbox.py @@ -1800,6 +1800,7 @@ def __init__(self, chain): self.df = c._frame self._org_idx = self.df.index self._edit_idx = range(0, len(self._org_idx)) + self._painted_idx = c.index self._idx_valmap = {n: o for n, o in zip(self._edit_idx, self._org_idx.get_level_values(1))} @@ -1807,6 +1808,7 @@ def __init__(self, chain): self._org_col = self.df.columns self._edit_col = range(0, len(self._org_col)) + self._painted_col = c.columns self._col_valmap = {n: o for n, o in zip(self._edit_col, self._org_col.get_level_values(1))} @@ -1847,10 +1849,12 @@ def _updated_index_tuples(self, axis): current = self.df.columns.values.tolist() mapped = self._col_valmap org_tuples = self._org_col.tolist() + painted_tuples = self._painted_col.tolist() else: current = self.df.index.values.tolist() mapped = self._idx_valmap org_tuples = self._org_idx.tolist() + painted_tuples = self._painted_idx.tolist() merged = [mapped[val] if val in mapped else val for val in current] # ================================================================ if (self.array_mi and axis == 1) or axis == 0: @@ -1860,19 +1864,22 @@ def _updated_index_tuples(self, axis): # ================================================================ i = d = 0 new_tuples = [] + new_painted_tuples = [] for merged_val in merged: idx = i-d if i-d != len(org_tuples) else i-d-1 if org_tuples[idx][1] == merged_val: new_tuples.append(org_tuples[idx]) + new_painted_tuples.append(painted_tuples[idx]) else: empties = ['*'] * self._nest_mul new_tuple = tuple(empties + [merged_val]) new_tuples.append(new_tuple) + new_painted_tuples.append(new_tuple) d += 1 i += 1 - return new_tuples + return new_tuples, new_painted_tuples - def _reindex(self): + def _reindex(self, chain): """ """ y_names = ['Question', 'Values'] @@ -1881,10 +1888,12 @@ def _reindex(self): else: x_names = ['Array', 'Questions'] if self.nested_y: y_names = y_names * (self._nest_mul - 1) - tuples = self._updated_index_tuples(axis=1) + tuples, p_tuples = self._updated_index_tuples(axis=1) self.df.columns = pd.MultiIndex.from_tuples(tuples, names=y_names) - tuples = self._updated_index_tuples(axis=0) + chain.columns = pd.MultiIndex.from_tuples(p_tuples, names=y_names) + tuples, p_tuples = self._updated_index_tuples(axis=0) self.df.index = pd.MultiIndex.from_tuples(tuples, names=x_names) + chain.index = pd.MultiIndex.from_tuples(p_tuples, names=x_names) return None def export(self): @@ -1897,7 +1906,7 @@ def assign(self, transformed_chain_df): """ if not isinstance(transformed_chain_df, self._TransformedChainDF): raise ValueError("Must pass an exported ``Chain`` instance!") - transformed_chain_df._reindex() + transformed_chain_df._reindex(self) self._frame = transformed_chain_df.df self.views = transformed_chain_df._transf_views return None @@ -2695,10 +2704,16 @@ def _describe_block(self, description, row_id): else: idx = self.dataframe.index.get_level_values(1).tolist() idx_view_map = zip(idx, vpr) - block_net_vk = [v for v in vpr if len(v.split('|')[2].split('['))>2 or - '[+{' in v.split('|')[2] or '}+]' in v.split('|')[2]] + + block_net_vk = [ + v for v in vpr + if not v == "__viewlike__" and ( + len(v.split('|')[2].split('['))>2 or + '[+{' in v.split('|')[2] or '}+]' in v.split('|')[2])] + has_calc = any([v.split('|')[1].startswith('f.c') for v in block_net_vk]) - is_tested = any(v.split('|')[1].startswith('t.props') for v in vpr) + is_tested = any(v.split('|')[1].startswith('t.props') for v in vpr + if not v == "__viewlike__" ) if block_net_vk: expr = block_net_vk[0].split('|')[2] expanded_codes = set(map(int, re.findall(r'\d+', expr))) @@ -3776,7 +3791,6 @@ def toggle_labels(self): values = self._frame.values self._frame.loc[:, :] = self.frame_values self.frame_values = values - return self @staticmethod From 30fb6aa919ca82d37363c774d57a880f338cfbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Thu, 8 Aug 2019 16:43:16 +0200 Subject: [PATCH 720/733] set textkey --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index d3e0e523f..86f36f401 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1331,7 +1331,7 @@ def to_dataset(self, mode=None, from_set="data file", additions="sort_within", ds._meta['masks'][v].pop('rules') else: ds._meta['columns'][v].pop('rules') - self.set_text_key(self.language) + ds.set_text_key(self.language) if "oe" in mode: self._apply_oe_replacements(ds) return ds From 1703f29df57b4caa359f07a0837e289b5bbfd62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 13 Aug 2019 09:47:01 +0200 Subject: [PATCH 721/733] fixture in to_dataset oe replacements --- quantipy/core/batch.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index d3e0e523f..7bb744446 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1365,12 +1365,14 @@ def _get_vlist(self, batch, mode): return vlist def _apply_oe_replacements(self, dataset): + numerical = ["int", "single", "is_delimited_set"] for oe in self.verbatims: + data = dataset._data[oe["columns"]] if oe['replace']: for target, repl in oe['replace'].items(): if not repl: repl = np.NaN - dataset._data.replace(target, repl, inplace=True) + data.replace(target, repl, inplace=True) if not oe['incl_nan']: - for col in oe['columns']: + if not self._get_type(col) in numerical: dataset._data[col].replace(np.NaN, '', inplace=True) From 91756581570faeba359a9d0237223c63598c59e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 13 Aug 2019 09:59:36 +0200 Subject: [PATCH 722/733] typo --- quantipy/core/batch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 7bb744446..3f1c9785e 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1374,5 +1374,6 @@ def _apply_oe_replacements(self, dataset): repl = np.NaN data.replace(target, repl, inplace=True) if not oe['incl_nan']: - if not self._get_type(col) in numerical: - dataset._data[col].replace(np.NaN, '', inplace=True) + for col in oe['columns']: + if col not in numerical: + dataset._data[col].replace(np.NaN, '', inplace=True) From fa34a66095991dcf3f966b794cd296c422fb06e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 13 Aug 2019 10:09:17 +0200 Subject: [PATCH 723/733] use different approach to avoid pandas warning --- quantipy/core/batch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index 3f1c9785e..d234fd12e 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1367,13 +1367,14 @@ def _get_vlist(self, batch, mode): def _apply_oe_replacements(self, dataset): numerical = ["int", "single", "is_delimited_set"] for oe in self.verbatims: - data = dataset._data[oe["columns"]] if oe['replace']: + data = dataset._data[oe["columns"]].copy() for target, repl in oe['replace'].items(): if not repl: repl = np.NaN data.replace(target, repl, inplace=True) + dataset._data[oe["columns"]] = data if not oe['incl_nan']: for col in oe['columns']: if col not in numerical: - dataset._data[col].replace(np.NaN, '', inplace=True) + dataset._data[col].replace(np.NaN, '', inplace=True) \ No newline at end of file From a6332b62422560a853c294b61c991e7b6ee6a30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerstin=20M=C3=BCller?= Date: Tue, 13 Aug 2019 11:17:43 +0200 Subject: [PATCH 724/733] typo --- quantipy/core/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/batch.py b/quantipy/core/batch.py index d234fd12e..9b6ed7821 100644 --- a/quantipy/core/batch.py +++ b/quantipy/core/batch.py @@ -1376,5 +1376,5 @@ def _apply_oe_replacements(self, dataset): dataset._data[oe["columns"]] = data if not oe['incl_nan']: for col in oe['columns']: - if col not in numerical: + if self._get_type(col) not in numerical: dataset._data[col].replace(np.NaN, '', inplace=True) \ No newline at end of file From 345c741a309c0c301c67b9fa7fb9518d0bf0c1ec Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Sep 2019 18:27:04 +0200 Subject: [PATCH 725/733] nfirst method --- quantipy/core/dataset.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 24af242bf..0c77760c7 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4063,6 +4063,37 @@ def dichotomize(self, name, value_texts=None, keep_variable_text=True, self.drop(name) return None + def nfirst(self, name, n_first=3, others='others', reduce_values=False): + """ + """ + self[name] = self[name].apply(lambda x: str(x) + ';' + if not str(x).endswith(';') else x) + values = self.values(name) + if others: + o_name = '{}_{}'.format(name, others) + o_label = '{}_{}'.format(self.text(name), others) + self.add_meta(o_name, 'delimited set', o_label, values) + for n in frange('1-{}'.format(n_first)): + n_name = '{}_{}'.format(name, n) + n_label = '{}_{}'.format(self.text(name), n) + self.add_meta(n_name, 'single', n_label, values) + n_vector = self[name].str.split(';', n=n, expand=True)[n-1] + self[n_name] = n_vector.replace(('', None), np.NaN).astype(float) + if reduce_values: + existing_codes = dataset.codes_in_data(n_name) + reduce_codes = [value[0] for value in values + if value[0] not in existing_codes] + self.remove_values(n_name, reduce_codes) + if others: + o_string = self[name].str.split(';', n=n, expand=True)[n_first] + self[o_name] = o_string.replace(('', None), np.NaN) + if reduce_values: + existing_codes = dataset.codes_in_data(o_name) + reduce_codes = [value[0] for value in values + if value[0] not in existing_codes] + self.remove_values(o_name, reduce_codes) + return None + @modify(to_list='codes') @verify(variables={'name': 'masks'}, text_keys='text_key') def flatten(self, name, codes, new_name=None, text_key=None): From 8abb165d29354885e095243506c3c14ef9841218 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Sep 2019 18:31:21 +0200 Subject: [PATCH 726/733] wrong local --- quantipy/core/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 0c77760c7..68cbbf71a 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4080,7 +4080,7 @@ def nfirst(self, name, n_first=3, others='others', reduce_values=False): n_vector = self[name].str.split(';', n=n, expand=True)[n-1] self[n_name] = n_vector.replace(('', None), np.NaN).astype(float) if reduce_values: - existing_codes = dataset.codes_in_data(n_name) + existing_codes = self.codes_in_data(n_name) reduce_codes = [value[0] for value in values if value[0] not in existing_codes] self.remove_values(n_name, reduce_codes) @@ -4088,7 +4088,7 @@ def nfirst(self, name, n_first=3, others='others', reduce_values=False): o_string = self[name].str.split(';', n=n, expand=True)[n_first] self[o_name] = o_string.replace(('', None), np.NaN) if reduce_values: - existing_codes = dataset.codes_in_data(o_name) + existing_codes = self.codes_in_data(o_name) reduce_codes = [value[0] for value in values if value[0] not in existing_codes] self.remove_values(o_name, reduce_codes) From e1fae7c5d2bf4acf5f029eeaf0220bdb4d9f35d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Sep 2019 18:39:54 +0200 Subject: [PATCH 727/733] adding better question labels --- quantipy/core/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 68cbbf71a..176092de8 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4071,11 +4071,11 @@ def nfirst(self, name, n_first=3, others='others', reduce_values=False): values = self.values(name) if others: o_name = '{}_{}'.format(name, others) - o_label = '{}_{}'.format(self.text(name), others) + o_label = '{} ({})'.format(self.text(name), others) self.add_meta(o_name, 'delimited set', o_label, values) for n in frange('1-{}'.format(n_first)): n_name = '{}_{}'.format(name, n) - n_label = '{}_{}'.format(self.text(name), n) + n_label = '{} ({})'.format(self.text(name), n) self.add_meta(n_name, 'single', n_label, values) n_vector = self[name].str.split(';', n=n, expand=True)[n-1] self[n_name] = n_vector.replace(('', None), np.NaN).astype(float) From d9d41cbfc156b1c79a0859b95f8feece7dbe6d11 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 12 Sep 2019 10:12:21 +0200 Subject: [PATCH 728/733] rename, clean-up --- quantipy/core/dataset.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 176092de8..1a4dcac57 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4063,35 +4063,32 @@ def dichotomize(self, name, value_texts=None, keep_variable_text=True, self.drop(name) return None - def nfirst(self, name, n_first=3, others='others', reduce_values=False): + def first_responses(self, name, n=3, others='others', reduce_values=False): """ """ self[name] = self[name].apply(lambda x: str(x) + ';' if not str(x).endswith(';') else x) + created = [] values = self.values(name) + for _n in frange('1-{}'.format(n)): + n_name = '{}_{}'.format(name, _n) + n_label = '{} ({})'.format(self.text(name), _n) + self.add_meta(n_name, 'single', n_label, values) + n_vector = self[name].str.split(';', n=_n, expand=True)[_n-1] + self[n_name] = n_vector.replace(('', None), np.NaN).astype(float) + created.append(n_name) if others: o_name = '{}_{}'.format(name, others) o_label = '{} ({})'.format(self.text(name), others) self.add_meta(o_name, 'delimited set', o_label, values) - for n in frange('1-{}'.format(n_first)): - n_name = '{}_{}'.format(name, n) - n_label = '{} ({})'.format(self.text(name), n) - self.add_meta(n_name, 'single', n_label, values) - n_vector = self[name].str.split(';', n=n, expand=True)[n-1] - self[n_name] = n_vector.replace(('', None), np.NaN).astype(float) - if reduce_values: - existing_codes = self.codes_in_data(n_name) - reduce_codes = [value[0] for value in values - if value[0] not in existing_codes] - self.remove_values(n_name, reduce_codes) - if others: - o_string = self[name].str.split(';', n=n, expand=True)[n_first] + o_string = self[name].str.split(';', n=n, expand=True)[n] self[o_name] = o_string.replace(('', None), np.NaN) - if reduce_values: - existing_codes = self.codes_in_data(o_name) + created.append(o_name) + if reduce_values: + for v in created: reduce_codes = [value[0] for value in values - if value[0] not in existing_codes] - self.remove_values(o_name, reduce_codes) + if value[0] not in self.codes_in_data(v)] + self.remove_values(v, reduce_codes) return None @modify(to_list='codes') From eaaeec664264a1535eb46c966bcce265f4735c71 Mon Sep 17 00:00:00 2001 From: jamesrkg Date: Thu, 12 Sep 2019 16:55:39 +0800 Subject: [PATCH 729/733] Removed apply from DataSet.first_mentions. --- quantipy/core/dataset.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 1a4dcac57..d9956ccf1 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4066,8 +4066,6 @@ def dichotomize(self, name, value_texts=None, keep_variable_text=True, def first_responses(self, name, n=3, others='others', reduce_values=False): """ """ - self[name] = self[name].apply(lambda x: str(x) + ';' - if not str(x).endswith(';') else x) created = [] values = self.values(name) for _n in frange('1-{}'.format(n)): From 1ebbe7e0ee1a3b83705a2b54f1075bd7f458e01b Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 13 Sep 2019 10:37:47 +0200 Subject: [PATCH 730/733] adding docstring, type check --- quantipy/core/dataset.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index d9956ccf1..4085ddc2e 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4063,9 +4063,36 @@ def dichotomize(self, name, value_texts=None, keep_variable_text=True, self.drop(name) return None + @verify(variables={'name': 'columns'}) def first_responses(self, name, n=3, others='others', reduce_values=False): """ + Create n-first mentions from the set of responses of a delimited set. + + Parameters + ---------- + name : str + The column variable name of a delimited set keyed in + ``meta['columns']``. + n : int, default 3 + The number of mentions that will be turned into single-type + variables, i.e. 1st mention, 2nd mention, 3rd mention, 4th mention, + etc. + others : None or str, default 'others' + If provided, all remaining values will end up in a new delimited + set variable reduced by the responses transferred to the single + mention variables. + reduce_values : bool, default False + If True, each new variable will only list the categorical value + metadata for the codes found in the respective data vector, i.e. + not the initial full codeframe. + + Returns + ------- + None + DataSet is modified inplace. """ + if self._get_type(name) != 'delimited set': + return None created = [] values = self.values(name) for _n in frange('1-{}'.format(n)): From 671fb5a9e4aac1f04d2c59f4c6f1c2aef1dd0e1f Mon Sep 17 00:00:00 2001 From: Geir Freysson Date: Mon, 23 Sep 2019 16:01:26 +0100 Subject: [PATCH 731/733] Update README.md with link to python 3 repo. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d1ce7342..c5a328e60 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,10 @@ Quantipy is an open-source data processing, analysis and reporting software proj #### Contributors - Alexander Buchhammer, Alasdair Eaglestone, James Griffiths, Kerstin Müller : https://yougov.co.uk -- Datasmoothie’s Birgir Hrafn Sigurðsson and Geir Freysson: http://datasmoothie.io/ +- Datasmoothie’s Birgir Hrafn Sigurðsson and Geir Freysson: http://datasmoothie.com/ + +### Python 3 compatability +Efforts are underway to port Quantipy to Python 3 in a [seperate repository](https://www.github.com/quantipy/quantipy3). ## Docs [View the documentation at readthedocs.org](http://quantipy.readthedocs.io/) From 745bb7f713a000482468636d640ce4a304bd10e4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Sep 2019 09:21:52 +0200 Subject: [PATCH 732/733] bugfix for empty variables --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index 4085ddc2e..a6f5983c2 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4091,7 +4091,7 @@ def first_responses(self, name, n=3, others='others', reduce_values=False): None DataSet is modified inplace. """ - if self._get_type(name) != 'delimited set': + if self._get_type(name) != 'delimited set' or self.empty(name): return None created = [] values = self.values(name) From cbfc6617390937ef905611ca577ff4f868113ba6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Sep 2019 10:00:47 +0200 Subject: [PATCH 733/733] Revert "bugfix for empty variables" This reverts commit 745bb7f713a000482468636d640ce4a304bd10e4. --- quantipy/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantipy/core/dataset.py b/quantipy/core/dataset.py index a6f5983c2..4085ddc2e 100644 --- a/quantipy/core/dataset.py +++ b/quantipy/core/dataset.py @@ -4091,7 +4091,7 @@ def first_responses(self, name, n=3, others='others', reduce_values=False): None DataSet is modified inplace. """ - if self._get_type(name) != 'delimited set' or self.empty(name): + if self._get_type(name) != 'delimited set': return None created = [] values = self.values(name)