dukit 0.2.6__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dukit
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: dukit - data utilities kit for exploring, modifying and visualizing small but messy data using pandas
5
5
  Keywords: pandas,query,utility,data,diff
6
6
  Author: MartinVoelkl94
@@ -89,8 +89,8 @@ these functions offer various ways to deal with non-unique keys/ids.
89
89
  primarily used when merging 2 dfs with a one-to-many relationship.
90
90
 
91
91
  available functions:
92
- - df.dk.flatten()
93
- - df.dk.stagger()
92
+ - df.dk.stack()
93
+ - df.dk.unstack()
94
94
  - df.dk.embed()
95
95
  - df.dk.collapse()
96
96
  <br>
@@ -72,8 +72,8 @@ these functions offer various ways to deal with non-unique keys/ids.
72
72
  primarily used when merging 2 dfs with a one-to-many relationship.
73
73
 
74
74
  available functions:
75
- - df.dk.flatten()
76
- - df.dk.stagger()
75
+ - df.dk.stack()
76
+ - df.dk.unstack()
77
77
  - df.dk.embed()
78
78
  - df.dk.collapse()
79
79
  <br>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dukit"
3
- version = "0.2.6"
3
+ version = "0.2.7"
4
4
  description = "dukit - data utilities kit for exploring, modifying and visualizing small but messy data using pandas"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "MartinVoelkl94", email = "martinv16248@gmail.com" }]
@@ -24,20 +24,24 @@ from .excel import (
24
24
  save,
25
25
  )
26
26
  from .pandas import (
27
- collapse,
27
+ compose,
28
+ decompose,
29
+ stack,
30
+ unstack,
31
+ transpose,
28
32
  date_delta,
29
33
  date_table,
30
34
  deduplicate,
31
- embed,
32
- flatten,
33
35
  get_df,
34
36
  get_df_patients,
35
37
  get_df_medications,
36
38
  get_df_old,
37
39
  get_df_new,
38
- stagger,
39
- transpose,
40
40
  split_col,
41
+ embed,
42
+ collapse,
43
+ stagger,
44
+ flatten,
41
45
  )
42
46
  from .os import (
43
47
  cd,
@@ -89,20 +93,24 @@ __all__ = (
89
93
  'format',
90
94
  'save',
91
95
 
92
- 'collapse',
96
+ 'compose',
97
+ 'decompose',
98
+ 'stack',
99
+ 'unstack',
100
+ 'transpose',
93
101
  'date_delta',
94
102
  'date_table',
95
103
  'deduplicate',
96
- 'embed',
97
- 'flatten',
98
104
  'get_df',
99
105
  'get_df_patients',
100
106
  'get_df_medications',
101
107
  'get_df_old',
102
108
  'get_df_new',
103
- 'stagger',
104
- 'transpose',
105
109
  'split_col',
110
+ 'embed',
111
+ 'collapse',
112
+ 'stagger',
113
+ 'flatten',
106
114
 
107
115
  'cd',
108
116
  'cp',
@@ -1498,15 +1498,26 @@ def _add_retained_cols(d: Diff) -> Diff:
1498
1498
 
1499
1499
  idx_shared = d._values.index.intersection(d._cols_retained.index)
1500
1500
  df_retained = d._cols_retained.loc[idx_shared, :]
1501
- d._values = pd.concat([df_retained, d._values], axis='columns')
1501
+ d._values = pd.merge(
1502
+ df_retained,
1503
+ d._values,
1504
+ left_index=True,
1505
+ right_index=True,
1506
+ how='right',
1507
+ )
1502
1508
 
1503
1509
  df_style = pd.DataFrame(
1504
1510
  data='',
1505
1511
  index=df_retained.index,
1506
1512
  columns=df_retained.columns,
1507
1513
  )
1508
- d._style = pd.concat([df_style, d._style], axis='columns')
1509
-
1514
+ d._style = pd.merge(
1515
+ df_style,
1516
+ d._style,
1517
+ left_index=True,
1518
+ right_index=True,
1519
+ how='right',
1520
+ )
1510
1521
  return d
1511
1522
 
1512
1523
 
@@ -9,13 +9,15 @@ from .qlang import (
9
9
  qs,
10
10
  )
11
11
  from .pandas import (
12
+ compose,
13
+ decompose,
14
+ stack,
15
+ unstack,
16
+ transpose,
12
17
  date_delta,
13
18
  date_table,
14
19
  collapse,
15
20
  embed,
16
- flatten,
17
- stagger,
18
- transpose,
19
21
  split_col,
20
22
  )
21
23
 
@@ -55,6 +57,124 @@ class DukitAccessor():
55
57
  return qr(self.df, code, verbosity)
56
58
 
57
59
 
60
+ def compose(
61
+ self,
62
+ col: str,
63
+ cols: list[str],
64
+ template='{0}_{1}',
65
+ verbosity=3,
66
+ ):
67
+ return compose(
68
+ self.df,
69
+ col=col,
70
+ cols=cols,
71
+ template=template,
72
+ verbosity=verbosity,
73
+ )
74
+
75
+
76
+ def stack(
77
+ self,
78
+ uid: str,
79
+ col_pattern: str = r'(?P<col>.+)_(?P<subid>.+)',
80
+ uid_template: str = '{uid}_{subid}',
81
+ cols_keep: list[str] | None = None,
82
+ dropna: bool = True,
83
+ sort: bool = True,
84
+ verbosity: int = 3,
85
+ ) -> pd.DataFrame:
86
+ df_new = stack(
87
+ self.df,
88
+ uid=uid,
89
+ col_pattern=col_pattern,
90
+ uid_template=uid_template,
91
+ cols_keep=cols_keep,
92
+ dropna=dropna,
93
+ sort=sort,
94
+ verbosity=verbosity,
95
+ )
96
+ return df_new
97
+
98
+
99
+ def unstack(
100
+ self,
101
+ uid: str,
102
+ uid_pattern: str = r'(?P<uid>.+)_(?P<subid>.+)',
103
+ col_template: str = '{col}_{subid}',
104
+ cols_keep: list[str] | None = None,
105
+ stagger: bool = True,
106
+ verbosity: int = 3,
107
+ ) -> pd.DataFrame:
108
+ df_new = unstack(
109
+ self.df,
110
+ uid=uid,
111
+ uid_pattern=uid_pattern,
112
+ col_template=col_template,
113
+ cols_keep=cols_keep,
114
+ stagger=stagger,
115
+ verbosity=verbosity
116
+ )
117
+ return df_new
118
+
119
+
120
+ def transpose(
121
+ self,
122
+ header='uid',
123
+ ) -> pd.DataFrame:
124
+ df_new = transpose(
125
+ self.df,
126
+ header=header,
127
+ )
128
+ return df_new
129
+
130
+
131
+ def save(
132
+ self,
133
+ path: str,
134
+ sheet_name='df',
135
+ index=False,
136
+ format_excel=True,
137
+ freeze_panes='B2',
138
+ hide_sheets=None,
139
+ hide_cols=None,
140
+ col_width_max=70,
141
+ col_width_padding=2,
142
+ align_vertical='top',
143
+ align_horizontal='left',
144
+ wrap_text=True,
145
+ **kwargs,
146
+ ):
147
+ save(
148
+ df=self.df,
149
+ path=path,
150
+ sheet_name=sheet_name,
151
+ index=index,
152
+ format_excel=format_excel,
153
+ freeze_panes=freeze_panes,
154
+ hide_sheets=hide_sheets,
155
+ hide_cols=hide_cols,
156
+ col_width_max=col_width_max,
157
+ col_width_padding=col_width_padding,
158
+ align_vertical=align_vertical,
159
+ align_horizontal=align_horizontal,
160
+ wrap_text=wrap_text,
161
+ **kwargs,
162
+ )
163
+ return None
164
+
165
+
166
+ def style(self):
167
+ code = r"""
168
+ .replace("\n", "<br>")
169
+ .wrap(normal)
170
+ .mono()
171
+ .align(left)
172
+ %.align(center)
173
+ """
174
+ df_styled = qs(self.df, code)
175
+ return df_styled
176
+
177
+
58
178
  def date_delta(
59
179
  self,
60
180
  reference_date: str | datetime.date | pd.Timestamp = None,
@@ -135,45 +255,6 @@ class DukitAccessor():
135
255
  return df_new
136
256
 
137
257
 
138
- def flatten(
139
- self,
140
- on: str,
141
- template='{colname}_#{counter}',
142
- ):
143
- df_new = flatten(
144
- self.df,
145
- on=on,
146
- template=template,
147
- )
148
- return df_new
149
-
150
-
151
- def stagger(
152
- self,
153
- on: str,
154
- template='#{counter}_{colname}',
155
- separator_col='#{counter}',
156
- ):
157
- df_new = stagger(
158
- self.df,
159
- on=on,
160
- template=template,
161
- separator_col=separator_col,
162
- )
163
- return df_new
164
-
165
-
166
- def transpose(
167
- self,
168
- header='uid',
169
- ) -> pd.DataFrame:
170
- df_new = transpose(
171
- self.df,
172
- header=header,
173
- )
174
- return df_new
175
-
176
-
177
258
  def split_col(
178
259
  self,
179
260
  col: str = 'uid',
@@ -189,50 +270,3 @@ class DukitAccessor():
189
270
  new2=new2,
190
271
  )
191
272
  return df_new
192
-
193
-
194
- def save(
195
- self,
196
- path: str,
197
- sheet_name='df',
198
- index=False,
199
- format_excel=True,
200
- freeze_panes='B2',
201
- hide_sheets=None,
202
- hide_cols=None,
203
- col_width_max=70,
204
- col_width_padding=2,
205
- align_vertical='top',
206
- align_horizontal='left',
207
- wrap_text=True,
208
- **kwargs,
209
- ):
210
- save(
211
- df=self.df,
212
- path=path,
213
- sheet_name=sheet_name,
214
- index=index,
215
- format_excel=format_excel,
216
- freeze_panes=freeze_panes,
217
- hide_sheets=hide_sheets,
218
- hide_cols=hide_cols,
219
- col_width_max=col_width_max,
220
- col_width_padding=col_width_padding,
221
- align_vertical=align_vertical,
222
- align_horizontal=align_horizontal,
223
- wrap_text=wrap_text,
224
- **kwargs,
225
- )
226
- return None
227
-
228
-
229
- def style(self):
230
- code = r"""
231
- .replace("\n", "<br>")
232
- .wrap(normal)
233
- .mono()
234
- .align(left)
235
- %.align(center)
236
- """
237
- df_styled = qs(self.df, code)
238
- return df_styled
@@ -1,11 +1,19 @@
1
1
 
2
+ import re
2
3
  import copy
3
4
  import datetime
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
 
7
- from .utils import log, ensure_unique_string, GREEN_LIGHT
8
- from .typing import date_
8
+ from .utils import (
9
+ log,
10
+ ensure_unique_string,
11
+ GREEN_LIGHT,
12
+ )
13
+ from .typing import (
14
+ date_,
15
+ list_,
16
+ )
9
17
 
10
18
 
11
19
  def get_df() -> pd.DataFrame:
@@ -295,6 +303,519 @@ def get_df_new() -> pd.DataFrame:
295
303
 
296
304
 
297
305
 
306
+ def deduplicate(
307
+ obj,
308
+ name='object',
309
+ rename_all=True,
310
+ start_at_1=True,
311
+ verbosity=3,
312
+ ):
313
+ """
314
+ make values unique by appending consecutive numeric suffixes.
315
+
316
+ Parameters
317
+ ----------
318
+ obj : object
319
+ object convertible to a pandas Series.
320
+ name : str, default 'object'
321
+ name used in diagnostic messages.
322
+ rename_all : bool, default True
323
+ whether to rename all values, not just duplicates.
324
+ start_at_1 : bool, default True
325
+ whether to start numbering at 0 or 1.
326
+ verbosity : int, default 3
327
+ logging verbosity level.
328
+
329
+ Returns
330
+ -------
331
+ object
332
+ deduplicated object, converted back to its original type when
333
+ possible. Values are converted to strings during processing.
334
+ """
335
+
336
+ obj = copy.deepcopy(obj)
337
+ class_orig = obj.__class__
338
+ obj = _to_series(obj)
339
+
340
+ #Values can only be deduplicated if index is unique
341
+ if not obj.index.is_unique:
342
+ msg = (
343
+ f'info: duplicates found in index of {name}.'
344
+ ' deduplicating by appending consecutive numbers.'
345
+ )
346
+ log(msg, 'dk.pandas.deduplicate', verbosity)
347
+ obj.index = _deduplicate(
348
+ _to_series(obj.index),
349
+ rename_all,
350
+ start_at_1,
351
+ )
352
+
353
+ if rename_all:
354
+ obj = _deduplicate(
355
+ obj,
356
+ rename_all,
357
+ start_at_1,
358
+ )
359
+ rounds = 1
360
+ else:
361
+ rounds = 0
362
+
363
+ while not obj.is_unique:
364
+ if rounds == 0:
365
+ msg = (
366
+ f'debug: duplicates found in {name}.'
367
+ ' deduplicating by appending consecutive numbers.'
368
+ )
369
+ else:
370
+ msg = (
371
+ f'debug: duplicates still found in {name} after'
372
+ f' {rounds} deduplication rounds.'
373
+ ' deduplicating again by appending consecutive numbers.'
374
+ )
375
+ log(msg, 'dk.pandas.deduplicate', verbosity)
376
+ obj = _deduplicate(
377
+ obj,
378
+ rename_all,
379
+ start_at_1,
380
+ )
381
+ rounds += 1
382
+
383
+ if not isinstance(obj, class_orig):
384
+ try:
385
+ obj = class_orig(obj)
386
+ except Exception as e:
387
+ msg = (
388
+ 'Error: could not convert deduplicated object'
389
+ f' back to original type {class_orig}: {e}'
390
+ )
391
+ log(msg, 'dk.pandas.deduplicate', verbosity)
392
+
393
+ return obj
394
+
395
+
396
+ def _to_series(obj, verbosity=3):
397
+ try:
398
+ obj = pd.Series(obj, dtype=str)
399
+ except Exception as e:
400
+ msg = (
401
+ 'Error: could not convert input of type'
402
+ f' {type(obj)} to Series: {e}'
403
+ )
404
+ log(msg, 'dk.pandas._to_series', verbosity)
405
+ return obj
406
+
407
+
408
+ def _deduplicate(series, rename_all=True, start_at_1=True):
409
+ cumulative_count = series.groupby(series).cumcount()
410
+ if rename_all:
411
+ if start_at_1:
412
+ cumulative_count += 1
413
+ series = (
414
+ series.astype(str)
415
+ + '_'
416
+ + cumulative_count.astype(str)
417
+ )
418
+ else:
419
+ duplicates_mask = series.index[cumulative_count > 0]
420
+ duplicates = series[duplicates_mask]
421
+ duplicates_new = (
422
+ duplicates.astype(str)
423
+ + '_'
424
+ + cumulative_count[duplicates_mask].astype(str)
425
+ )
426
+ series[duplicates_mask] = duplicates_new
427
+ return series
428
+
429
+
430
+
431
+ def compose(
432
+ df: pd.DataFrame,
433
+ col: str,
434
+ cols: list[str],
435
+ template='{0}_{1}',
436
+ verbosity=3,
437
+ ):
438
+
439
+ _compose_check_if_col_alread_exists(df, col, verbosity)
440
+ _compose_check_cols_matches_template(cols, template, verbosity)
441
+ pos = _compose_get_first_col_pos(df, cols, verbosity)
442
+
443
+ col_new = df[cols].apply(lambda row: template.format(*row), axis='columns')
444
+ df.insert(pos, col, col_new)
445
+ df.drop(columns=cols, inplace=True)
446
+
447
+ return df
448
+
449
+
450
+ def _compose_check_cols_matches_template(cols, template, verbosity):
451
+ num_placeholders = template.count('{')
452
+ if num_placeholders != len(cols):
453
+ msg = (
454
+ 'WARNING: Number of placeholders in template'
455
+ f' ({num_placeholders}) does not match number'
456
+ f' of cols ({len(cols)}) provided.'
457
+ )
458
+ log(msg, 'dk.pandas.compose', verbosity)
459
+
460
+
461
+ def _compose_check_if_col_alread_exists(df, col, verbosity):
462
+ if col in df.columns:
463
+ msg = f'WARNING: col "{col}" already exists and will be overwritten.'
464
+ log(msg, 'dk.pandas.compose', verbosity)
465
+
466
+
467
+ def _compose_get_first_col_pos(df, cols, verbosity=3):
468
+ pos = len(df.columns)
469
+ for col_temp in cols:
470
+ if col_temp not in df.columns:
471
+ msg = f'ERROR: col "{col_temp}" does not exist.'
472
+ log(msg, 'dk.pandas.compose', verbosity)
473
+ raise ValueError(f'Col "{col_temp}" does not exist.')
474
+ pos_temp = df.columns.get_loc(col_temp)
475
+ if pos_temp < pos:
476
+ pos = pos_temp
477
+ return pos
478
+
479
+
480
+
481
+ def decompose(
482
+ df: pd.DataFrame,
483
+ col: str,
484
+ cols: list[str] | None = None,
485
+ pattern='(.+)_(.+)',
486
+ verbosity=3,
487
+ ):
488
+
489
+ _decomposes_check_if_col_exists(df, col, verbosity)
490
+
491
+ pos = df.columns.get_loc(col)
492
+ df_unmerged = df[col].str.extract(pattern)
493
+
494
+ if cols is None:
495
+ cols = [
496
+ f'{col}_{group}'for group
497
+ in range(1, len(df_unmerged.columns) + 1)
498
+ ]
499
+ else:
500
+ _decomposes_check_cols_matches_extraction(cols, df_unmerged, verbosity)
501
+ _decomposes_check_cols_already_exists(df, cols, verbosity)
502
+ df_unmerged.columns = cols
503
+
504
+ df1 = df.iloc[:, :pos]
505
+ df2 = df.iloc[:, pos+1:]
506
+ df_new = pd.concat([df1, df_unmerged, df2], axis='columns')
507
+
508
+ return df_new
509
+
510
+
511
+ def _decomposes_check_if_col_exists(df, col, verbosity):
512
+ if col not in df.columns:
513
+ msg = f'ERROR: col "{col}" does not exist.'
514
+ log(msg, 'dk.pandas.decompose', verbosity)
515
+ raise ValueError(msg)
516
+
517
+
518
+ def _decomposes_check_cols_matches_extraction(cols, df_unmerged, verbosity):
519
+ if len(cols) != len(df_unmerged.columns):
520
+ msg = (
521
+ f'ERROR: Number of columns provided ({len(cols)}) '
522
+ 'does not match number of groups extracted '
523
+ f'({len(df_unmerged.columns)}).'
524
+ )
525
+ log(msg, 'dk.pandas.decompose', verbosity)
526
+ raise ValueError(msg)
527
+
528
+
529
+ def _decomposes_check_cols_already_exists(df, cols, verbosity):
530
+ for col_temp in cols:
531
+ if col_temp in df.columns:
532
+ msg = f'WARNING: col "{col_temp}" already exists and will be overwritten.'
533
+ log(msg, 'dk.pandas.decompose', verbosity)
534
+
535
+
536
+
537
+ def stack(
538
+ df: pd.DataFrame,
539
+ uid: str,
540
+ col_pattern: str = r'(?P<col>.+)_(?P<subid>.+)',
541
+ uid_template: str = '{uid}_{subid}',
542
+ cols_keep: list[str] | None = None,
543
+ dropna: bool = True,
544
+ sort: bool = True,
545
+ verbosity: int = 3,
546
+ ) -> pd.DataFrame:
547
+
548
+ df = df.copy()
549
+ df[uid] = df[uid].astype('string')
550
+ _stack_check_uid_unique1(df, uid, verbosity)
551
+
552
+ uid_old = ensure_unique_string(uid, df.columns)
553
+ df.set_index(uid, inplace=True)
554
+ df.index.name = uid_old
555
+ cols_keep = list_(cols_keep)
556
+ cols_stack = [col for col in df.columns if col not in cols_keep]
557
+
558
+ non_na_vals = int(
559
+ df[cols_stack]
560
+ .notna()
561
+ .sum()
562
+ .sum()
563
+ )
564
+
565
+ subdfs = {}
566
+ not_matched = []
567
+ for col_old in cols_stack:
568
+ match = re.match(col_pattern, col_old)
569
+ if match:
570
+ subid_val = match.group('subid')
571
+ col_new = match.group('col')
572
+ if subid_val not in subdfs:
573
+ subdfs[subid_val] = pd.DataFrame()
574
+ subdfs[subid_val][col_new] = df[col_old]
575
+ else:
576
+ not_matched.append(col_old)
577
+ _stack_check_successfull_match(not_matched, verbosity)
578
+
579
+ for subid_val in subdfs.keys():
580
+ subdf = subdfs[subid_val]
581
+ if dropna:
582
+ subdf.dropna(
583
+ inplace=True,
584
+ how='all',
585
+ )
586
+ uid_vals = [
587
+ uid_template.format(uid=uid_old, subid=subid_val)
588
+ for uid_old in subdf.index
589
+ ]
590
+ subdf.insert(0, uid, uid_vals)
591
+
592
+ df_new = pd.concat(list(subdfs.values()), axis='index')
593
+ df_new = pd.merge(
594
+ df[cols_keep].reset_index(),
595
+ df_new.reset_index(),
596
+ on=uid_old,
597
+ how='right',
598
+ )
599
+ df_new.drop(columns=[uid_old], inplace=True)
600
+
601
+ _stack_check_missed_values(df_new, uid, cols_keep, non_na_vals, verbosity)
602
+
603
+ if sort:
604
+ df_new.sort_values(by=uid, inplace=True)
605
+ df_new.reset_index(drop=True, inplace=True)
606
+
607
+ _stack_check_uid_unique2(uid, verbosity, df_new)
608
+
609
+ return df_new
610
+
611
+
612
+ def _stack_check_uid_unique1(df, uid, verbosity):
613
+ if not df[uid].is_unique:
614
+ non_unique = df[uid][df[uid].duplicated()].unique()
615
+ msg = (
616
+ f'WARNING: Source uid col "{uid}" '
617
+ f'contains non-unique values: {non_unique}'
618
+ )
619
+ log(msg, 'dk.pandas.stack', verbosity)
620
+
621
+
622
+ def _stack_check_successfull_match(not_matched, verbosity):
623
+ if len(not_matched) > 0:
624
+ msg = (
625
+ 'WARNING: Some cols did not '
626
+ f'match col_pattern: {not_matched}'
627
+ )
628
+ log(msg, 'dk.pandas.stack', verbosity)
629
+
630
+
631
+ def _stack_check_missed_values(df_new, uid, cols_keep, non_na_vals1, verbosity):
632
+ cols_stacked = [
633
+ col for col
634
+ in df_new.columns
635
+ if col not in cols_keep + [uid]
636
+ ]
637
+ non_na_vals2 = int(
638
+ df_new[cols_stacked]
639
+ .notna()
640
+ .sum()
641
+ .sum()
642
+ )
643
+ if non_na_vals1 != non_na_vals2:
644
+ msg = (
645
+ f'WARNING: Non-NA value mismatch: '
646
+ f'before={non_na_vals1}, after={non_na_vals2}'
647
+ )
648
+ log(msg, 'dk.pandas.stack', verbosity)
649
+
650
+
651
+ def _stack_check_uid_unique2(uid, verbosity, df_new):
652
+ if not df_new[uid].is_unique:
653
+ non_unique = df_new[uid][df_new[uid].duplicated()].unique()
654
+ msg = (
655
+ f'WARNING: Result uid col "{uid}" '
656
+ f'contains non-unique values: {non_unique}'
657
+ )
658
+ log(msg, 'dk.pandas.stack', verbosity)
659
+
660
+
661
+
662
+ def unstack(
663
+ df: pd.DataFrame,
664
+ uid: str,
665
+ uid_pattern: str = r'(?P<uid>.+)_(?P<subid>.+)',
666
+ col_template: str = '{col}_{subid}',
667
+ cols_keep: list[str] | None = None,
668
+ stagger: bool = True,
669
+ verbosity: int = 3,
670
+ ) -> pd.DataFrame:
671
+
672
+ df = df.copy()
673
+ df[uid] = df[uid].astype('string')
674
+ _unstack_check_uid_unique(df, uid, verbosity)
675
+
676
+ ids = df[uid].str.extract(uid_pattern)
677
+ expected_cols = ['uid', 'subid']
678
+ _unstack_check_successfull_extraction(df, uid, ids, expected_cols, verbosity)
679
+
680
+
681
+ ids.set_index('uid', inplace=True)
682
+ df[uid] = ids.index
683
+ df.set_index(uid, inplace=True)
684
+ subid_col = ids['subid']
685
+ subid_vals = subid_col.unique()
686
+ cols_keep = list_(cols_keep)
687
+ cols_unstack = [col for col in df.columns if col not in cols_keep]
688
+
689
+ non_na_vals = int(
690
+ df[cols_unstack]
691
+ .notna()
692
+ .sum()
693
+ .sum()
694
+ )
695
+
696
+ cols_unstacked = []
697
+ if stagger:
698
+ for subid_val in subid_vals:
699
+ mask = subid_col == subid_val
700
+ for col_old in cols_unstack:
701
+ col_unstacked = df.loc[mask, col_old]
702
+ col_unstacked.name = col_template.format(col=col_old, subid=subid_val)
703
+ cols_unstacked.append(col_unstacked)
704
+ else:
705
+ for col_old in cols_unstack:
706
+ for subid_val in subid_vals:
707
+ mask = subid_col == subid_val
708
+ col_unstacked = df.loc[mask, col_old]
709
+ col_unstacked.name = col_template.format(col=col_old, subid=subid_val)
710
+ cols_unstacked.append(col_unstacked)
711
+
712
+
713
+ df_new = df.loc[~df.index.duplicated(keep='first'), cols_keep]
714
+ for col_unstacked in cols_unstacked:
715
+ df_new = pd.merge(
716
+ df_new,
717
+ col_unstacked,
718
+ left_index=True,
719
+ right_index=True,
720
+ how='left'
721
+ )
722
+
723
+ _unstack_check_missed_values(uid, cols_keep, verbosity, non_na_vals, df_new)
724
+
725
+ df_new.reset_index(inplace=True)
726
+
727
+ return df_new
728
+
729
+
730
+ def _unstack_check_uid_unique(df, uid, verbosity):
731
+ if not df[uid].is_unique:
732
+ non_unique = (
733
+ df[uid]
734
+ [df[uid].duplicated()]
735
+ .unique()
736
+ .tolist()
737
+ )
738
+ msg = (
739
+ f'INFO: deduplicating non-unique values '
740
+ f'in source uid col "{uid}": {non_unique}'
741
+ )
742
+ log(msg, 'dk.pandas.unstack', verbosity)
743
+ df[uid] = deduplicate(df[uid])
744
+
745
+
746
+ def _unstack_check_successfull_extraction(df, uid, ids, expected_cols, verbosity):
747
+ if list(ids.columns) != expected_cols:
748
+ msg = (
749
+ 'WARNING: uid pattern must contain exactly 2 named '
750
+ 'match groups, one called uid and one called subid. '
751
+ 'use the subid group in col_template.'
752
+ )
753
+ log(msg, 'dk.pandas.unstack', verbosity)
754
+ else:
755
+ uid_isna = ids.iloc[:, 0].isna()
756
+ uids_unmatched = df.loc[uid_isna, uid].tolist()
757
+ if uid_isna.any():
758
+ msg = (
759
+ 'WARNING: Some uids did not match '
760
+ f'uid_pattern: {uids_unmatched}'
761
+ )
762
+ log(msg, 'dk.pandas.unstack', verbosity)
763
+
764
+
765
+ def _unstack_check_missed_values(uid, cols_keep, verbosity, non_na_vals1, df_new):
766
+ cols_stacked = [
767
+ col for col
768
+ in df_new.columns
769
+ if col not in cols_keep + [uid]
770
+ ]
771
+ non_na_vals2 = int(
772
+ df_new[cols_stacked]
773
+ .notna()
774
+ .sum()
775
+ .sum()
776
+ )
777
+ if non_na_vals1 != non_na_vals2:
778
+ msg = (
779
+ 'WARNING: Non-NA value mismatch: '
780
+ f'before={non_na_vals1}, after={non_na_vals2}'
781
+ )
782
+ log(msg, 'dk.pandas.unstack', verbosity)
783
+
784
+
785
+
786
+ def transpose(
787
+ df: pd.DataFrame,
788
+ header='uid',
789
+ ) -> pd.DataFrame:
790
+ """
791
+ transpose a df while preserving a designated header col.
792
+
793
+ Parameters
794
+ ----------
795
+ df : pandas.DataFrame
796
+ df to transpose.
797
+ header : str, default 'uid'
798
+ col used as the new header identifier.
799
+
800
+ Returns
801
+ -------
802
+ pandas.DataFrame
803
+ transposed df.
804
+ """
805
+
806
+ cols_old = df.columns
807
+ cols_new = df[header].values
808
+
809
+ df = df.T
810
+ df.columns = cols_new
811
+ df.insert(0, header, cols_old)
812
+ df.drop(header, axis=0, inplace=True)
813
+ df.reset_index(drop=True, inplace=True)
814
+
815
+ return df
816
+
817
+
818
+
298
819
  def date_delta(
299
820
  df: pd.DataFrame,
300
821
  reference_date: str | datetime.date | pd.Timestamp = None,
@@ -493,19 +1014,19 @@ def date_table(
493
1014
  cols_records = [
494
1015
  col for col
495
1016
  in df_timeline.columns
496
- if col not in ('_meta', 'days', 'planned')
1017
+ if col not in ('meta', 'days', 'planned')
497
1018
  ]
498
1019
  df_timeline.insert(
499
1020
  loc=0,
500
- column='_meta',
1021
+ column='meta',
501
1022
  value=''
502
1023
  )
503
1024
  has_event = df_timeline[cols_records] != filler
504
1025
  records_per_row = has_event.sum(axis='columns')
505
1026
  rows_with_records = records_per_row > 0
506
1027
  summary = 'records: ' + records_per_row.astype(str)
507
- df_timeline.loc[rows_with_records, '_meta'] = summary
508
- df_timeline['_meta'] = df_timeline['_meta'].fillna('')
1028
+ df_timeline.loc[rows_with_records, 'meta'] = summary
1029
+ df_timeline['meta'] = df_timeline['meta'].fillna('')
509
1030
 
510
1031
  if schedule is not None:
511
1032
  df_timeline = _highlight_schedule(df_timeline, schedule)
@@ -548,93 +1069,39 @@ def _highlight_schedule(
548
1069
 
549
1070
 
550
1071
 
1072
+ def split_col(
1073
+ df: pd.DataFrame,
1074
+ col: str = 'uid',
1075
+ sep: str = '::\n',
1076
+ new1: str = None,
1077
+ new2: str = 'label',
1078
+ ):
551
1079
 
552
- def deduplicate(obj, name='object', verbosity=3):
553
- """
554
- make values unique by appending consecutive numeric suffixes.
555
-
556
- Parameters
557
- ----------
558
- obj : object
559
- object convertible to a pandas Series.
560
- name : str, default 'object'
561
- name used in diagnostic messages.
562
- verbosity : int, default 3
563
- logging verbosity level.
564
-
565
- Returns
566
- -------
567
- object
568
- deduplicated object, converted back to its original type when
569
- possible. Values are converted to strings during processing.
570
- """
571
-
572
- obj = copy.deepcopy(obj)
573
- class_orig = obj.__class__
574
- obj = _to_series(obj)
575
-
576
- #Values can only be deduplicated if index is unique
577
- if not obj.index.is_unique:
578
- msg = (
579
- f'info: duplicates found in index of {name}.'
580
- ' deduplicating by appending consecutive numbers.'
581
- )
582
- log(msg, 'dk.pandas.deduplicate', verbosity)
583
- obj.index = _deduplicate(_to_series(obj.index))
584
-
585
- rounds = 0
586
- while not obj.is_unique:
587
- if rounds == 0:
588
- msg = (
589
- f'debug: duplicates found in {name}.'
590
- ' deduplicating by appending consecutive numbers.'
591
- )
592
- else:
593
- msg = (
594
- f'debug: duplicates still found in {name} after'
595
- f' {rounds} deduplication rounds.'
596
- ' deduplicating again by appending consecutive numbers.'
597
- )
598
- log(msg, 'dk.pandas.deduplicate', verbosity)
599
- obj = _deduplicate(obj)
600
- rounds += 1
601
-
602
- if not isinstance(obj, class_orig):
603
- try:
604
- obj = class_orig(obj)
605
- except Exception as e:
606
- msg = (
607
- 'Error: could not convert deduplicated object'
608
- f' back to original type {class_orig}: {e}'
609
- )
610
- log(msg, 'dk.pandas.deduplicate', verbosity)
611
-
612
- return obj
1080
+ if col not in df.columns:
1081
+ log(f'ERROR: no col named "{col}" found')
1082
+ return df
1083
+ elif df.columns.tolist().count(col) > 1:
1084
+ log(f'ERROR: multiple cols named "{col}" found')
1085
+ return df
613
1086
 
1087
+ df = df.copy()
1088
+ pos = df.columns.get_loc(col)
1089
+ splits = df[col].str.partition(sep)
614
1090
 
615
- def _to_series(obj, verbosity=3):
616
- try:
617
- obj = pd.Series(obj, dtype=str)
618
- except Exception as e:
619
- msg = (
620
- 'Error: could not convert input of type'
621
- f' {type(obj)} to Series: {e}'
622
- )
623
- log(msg, 'dk.pandas._to_series', verbosity)
624
- return obj
1091
+ df.drop(columns=[col], inplace=True)
1092
+ if new1 is None:
1093
+ new1 = col
1094
+ elif new1 in df.columns:
1095
+ log(f'WARNING: Col {new1} already exists, choosing new name.')
1096
+ new1 = ensure_unique_string(new1, df.columns.tolist())
1097
+ if new2 in df.columns:
1098
+ log(f'WARNING: Col {new2} already exists, choosing new name.')
1099
+ new2 = ensure_unique_string(new2, df.columns.tolist() + [new1])
625
1100
 
1101
+ df.insert(pos, new1, splits[0]) #type: ignore
1102
+ df.insert(pos + 1, new2, splits[2]) #type: ignore
626
1103
 
627
- def _deduplicate(series):
628
- cumulative_count = series.groupby(series).cumcount()
629
- duplicates_mask = series.index[cumulative_count > 0]
630
- duplicates = series[duplicates_mask]
631
- duplicates_new = (
632
- duplicates.astype(str)
633
- + '_'
634
- + cumulative_count[duplicates_mask].astype(str)
635
- )
636
- series[duplicates_mask] = duplicates_new
637
- return series
1104
+ return df
638
1105
 
639
1106
 
640
1107
 
@@ -854,72 +1321,3 @@ def _get_from_list(x, i):
854
1321
  return x[i]
855
1322
  else:
856
1323
  return None
857
-
858
-
859
-
860
- def transpose(
861
- df: pd.DataFrame,
862
- header='uid',
863
- ) -> pd.DataFrame:
864
- """
865
- transpose a df while preserving a designated header col.
866
-
867
- Parameters
868
- ----------
869
- df : pandas.DataFrame
870
- df to transpose.
871
- header : str, default 'uid'
872
- col used as the new header identifier.
873
-
874
- Returns
875
- -------
876
- pandas.DataFrame
877
- transposed df.
878
- """
879
-
880
- cols_old = df.columns
881
- cols_new = df[header].values
882
-
883
- df = df.T
884
- df.columns = cols_new
885
- df.insert(0, header, cols_old)
886
- df.drop(header, axis=0, inplace=True)
887
- df.reset_index(drop=True, inplace=True)
888
-
889
- return df
890
-
891
-
892
-
893
- def split_col(
894
- df: pd.DataFrame,
895
- col: str = 'uid',
896
- sep: str = '::\n',
897
- new1: str = None,
898
- new2: str = 'label',
899
- ):
900
-
901
- if col not in df.columns:
902
- log(f'ERROR: no col named "{col}" found')
903
- return df
904
- elif df.columns.tolist().count(col) > 1:
905
- log(f'ERROR: multiple cols named "{col}" found')
906
- return df
907
-
908
- df = df.copy()
909
- pos = df.columns.get_loc(col)
910
- splits = df[col].str.partition(sep)
911
-
912
- df.drop(columns=[col], inplace=True)
913
- if new1 is None:
914
- new1 = col
915
- elif new1 in df.columns:
916
- log(f'WARNING: Col {new1} already exists, choosing new name.')
917
- new1 = ensure_unique_string(new1, df.columns.tolist())
918
- if new2 in df.columns:
919
- log(f'WARNING: Col {new2} already exists, choosing new name.')
920
- new2 = ensure_unique_string(new2, df.columns.tolist() + [new1])
921
-
922
- df.insert(pos, new1, splits[0]) #type: ignore
923
- df.insert(pos + 1, new2, splits[2]) #type: ignore
924
-
925
- return df
@@ -821,7 +821,7 @@ class TagMetadata(Symbol):
821
821
  add a tag about the currently
822
822
  selected rows into the metadata col.
823
823
 
824
- assumes the metadata col is named "_meta"
824
+ assumes the metadata col is named "meta"
825
825
  and creates it if it doesn't exist.
826
826
 
827
827
  Examples
@@ -860,7 +860,7 @@ class TagMetadata(Symbol):
860
860
  if len(self.args) == 2:
861
861
  col_meta = str(self.args[1])
862
862
  else:
863
- col_meta = '_meta'
863
+ col_meta = 'meta'
864
864
 
865
865
  if col_meta not in q.df.columns:
866
866
  q = _insert_col_meta(col_meta, q)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes