dukit 0.2.5__tar.gz → 0.2.6__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.5
3
+ Version: 0.2.6
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dukit"
3
- version = "0.2.5"
3
+ version = "0.2.6"
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" }]
@@ -31,9 +31,13 @@ from .pandas import (
31
31
  embed,
32
32
  flatten,
33
33
  get_df,
34
- get_dfs,
34
+ get_df_patients,
35
+ get_df_medications,
36
+ get_df_old,
37
+ get_df_new,
35
38
  stagger,
36
39
  transpose,
40
+ split_col,
37
41
  )
38
42
  from .os import (
39
43
  cd,
@@ -92,9 +96,13 @@ __all__ = (
92
96
  'embed',
93
97
  'flatten',
94
98
  'get_df',
95
- 'get_dfs',
99
+ 'get_df_patients',
100
+ 'get_df_medications',
101
+ 'get_df_old',
102
+ 'get_df_new',
96
103
  'stagger',
97
104
  'transpose',
105
+ 'split_col',
98
106
 
99
107
  'cd',
100
108
  'cp',
@@ -33,51 +33,6 @@ def _setup_xlsx(df_old, df_new, tmpdir):
33
33
 
34
34
 
35
35
 
36
- def _get_dfs():
37
-
38
- df_old = pd.DataFrame(
39
- columns=['a', 'b', 'c'],
40
- index=['x', 'y', 'z', ],
41
- )
42
-
43
- df_old.insert(0, 'uid', df_old.index)
44
-
45
- df_old.loc['x', 'a'] = 1
46
- df_old.loc['x', 'b'] = 1
47
- df_old.loc['x', 'c'] = 1
48
-
49
- df_old.loc['y', 'a'] = 2
50
- df_old.loc['y', 'b'] = 2
51
- df_old.loc['y', 'c'] = 2
52
-
53
- df_old.loc['z', 'a'] = 3
54
- df_old.loc['z', 'b'] = None
55
- df_old.loc['z', 'c'] = 3
56
-
57
-
58
- df_new = pd.DataFrame(
59
- columns=['d', 'b', 'a'],
60
- index=['y', 'x2', 'z', ],
61
- )
62
-
63
- df_new.insert(0, 'uid', df_new.index)
64
-
65
- df_new.loc['y', 'd'] = 2
66
- df_new.loc['y', 'b'] = 2
67
- df_new.loc['y', 'a'] = 0
68
-
69
- df_new.loc['x2', 'd'] = 1
70
- df_new.loc['x2', 'b'] = 1
71
- df_new.loc['x2', 'a'] = 1
72
-
73
- df_new.loc['z', 'd'] = 3
74
- df_new.loc['z', 'b'] = 3
75
- df_new.loc['z', 'a'] = pd.NA
76
-
77
- return df_old, df_new
78
-
79
-
80
-
81
36
  def _get_expected_new():
82
37
 
83
38
  uid = pd.Series(
@@ -86,7 +41,7 @@ def _get_expected_new():
86
41
  name='uid',
87
42
  )
88
43
  expected = pd.DataFrame(
89
- columns=['uid', 'diff', 'd', 'b', 'a'],
44
+ columns=['diff', 'uid', 'd', 'b', 'a'],
90
45
  index=uid,
91
46
  )
92
47
 
@@ -121,7 +76,7 @@ def _get_expected_newplus():
121
76
  name='uid',
122
77
  )
123
78
  expected = pd.DataFrame(
124
- columns=['uid', 'diff', 'd', 'b', 'b *old', 'a', 'a *old'],
79
+ columns=['diff', 'uid', 'd', 'b', 'b *old', 'a', 'a *old'],
125
80
  index=uid,
126
81
  )
127
82
 
@@ -165,7 +120,7 @@ def _get_expected_old():
165
120
  name='uid',
166
121
  )
167
122
  expected = pd.DataFrame(
168
- columns=['uid', 'diff', 'a', 'b', 'c'],
123
+ columns=['diff', 'uid', 'a', 'b', 'c'],
169
124
  index=uid,
170
125
  )
171
126
 
@@ -200,7 +155,7 @@ def _get_expected_mix():
200
155
  name='uid',
201
156
  )
202
157
  expected = pd.DataFrame(
203
- columns=['uid', 'diff', 'd', 'b', 'a', 'c'],
158
+ columns=['diff', 'uid', 'd', 'b', 'a', 'c'],
204
159
  index=uid,
205
160
  )
206
161
 
@@ -27,6 +27,7 @@ def diff(
27
27
  remove_cols: list | str = None,
28
28
  remove_cols_by_suffix='',
29
29
  retain_cols: list | str = None,
30
+ retain_rows: list | str = None,
30
31
  ignore_cols: list | str = None,
31
32
  remove_sheets: list | str = None,
32
33
  name='data',
@@ -50,11 +51,11 @@ def diff(
50
51
  controls the values included in each result.
51
52
  - 'mix': show rows and cols from old and new df.
52
53
  additions and deletions are highlighted.
53
- - 'old': show only rows and cols from the old df.
54
- deletions are highlighted.
55
54
  - 'new': show only rows and cols from the new df.
56
55
  additions are highlighted.
57
56
  - 'new+': also preserve changed old values in parallel metadata cols.
57
+ - 'old': show only rows and cols from the old df.
58
+ deletions are highlighted.
58
59
  rename_cols : dict, optional
59
60
  mapping of original col names to names used during comparison.
60
61
  remove_cols : list or str, optional
@@ -62,7 +63,9 @@ def diff(
62
63
  remove_cols_by_suffix : str, default ''
63
64
  remove cols whose names end with this suffix.
64
65
  retain_cols : list or str, optional
65
- keep only the cols from the old df.
66
+ keep these cols from the old df.
67
+ retain_rows : list or str, optional
68
+ keep these rows from the old df.
66
69
  ignore_cols : list or str, optional
67
70
  cols excluded from comparison.
68
71
  remove_sheets : list or str, optional
@@ -111,6 +114,7 @@ def diff(
111
114
  remove_cols=remove_cols,
112
115
  remove_cols_by_suffix=remove_cols_by_suffix,
113
116
  retain_cols=retain_cols,
117
+ retain_rows=retain_rows,
114
118
  ignore_cols=ignore_cols,
115
119
  remove_sheets=remove_sheets,
116
120
  name=name,
@@ -132,6 +136,7 @@ def rediff(
132
136
  remove_cols: list | str = 'diff',
133
137
  remove_cols_by_suffix=' *old',
134
138
  retain_cols: list | str = 'notes',
139
+ retain_rows: list | str = 'notes',
135
140
  ignore_cols: list | str = None,
136
141
  remove_sheets=['info', 'summary', 'details'],
137
142
  name='data',
@@ -154,6 +159,7 @@ def rediff(
154
159
  remove_cols=remove_cols,
155
160
  remove_cols_by_suffix=remove_cols_by_suffix,
156
161
  retain_cols=retain_cols,
162
+ retain_rows=retain_rows,
157
163
  ignore_cols=ignore_cols,
158
164
  remove_sheets=remove_sheets,
159
165
  name=name,
@@ -182,6 +188,7 @@ class Diffs:
182
188
  remove_cols: list | str = None,
183
189
  remove_cols_by_suffix='',
184
190
  retain_cols: list | str = None,
191
+ retain_rows: list | str = None,
185
192
  ignore_cols: list | str = None,
186
193
  remove_sheets: list | str = None,
187
194
  name='data',
@@ -232,6 +239,7 @@ class Diffs:
232
239
  remove_cols=remove_cols,
233
240
  remove_cols_by_suffix=remove_cols_by_suffix,
234
241
  retain_cols=retain_cols,
242
+ retain_rows=retain_rows,
235
243
  ignore_cols=ignore_cols,
236
244
  linebreak=linebreak,
237
245
  suffix_old=suffix_old,
@@ -641,6 +649,7 @@ def _get_diffs(
641
649
  remove_cols: list | str = None,
642
650
  remove_cols_by_suffix='',
643
651
  retain_cols: list | str = None,
652
+ retain_rows: list | str = None,
644
653
  ignore_cols: list | str = None,
645
654
  linebreak='<br>',
646
655
  suffix_old=' *old',
@@ -660,6 +669,7 @@ def _get_diffs(
660
669
  mode=mode,
661
670
  rename_cols=rename_cols,
662
671
  retain_cols=retain_cols,
672
+ retain_rows=retain_rows,
663
673
  remove_cols=remove_cols,
664
674
  remove_cols_by_suffix=remove_cols_by_suffix,
665
675
  ignore_cols=ignore_cols,
@@ -677,6 +687,7 @@ def _get_diffs(
677
687
  mode=mode,
678
688
  rename_cols=rename_cols,
679
689
  retain_cols=retain_cols,
690
+ retain_rows=retain_rows,
680
691
  remove_cols=remove_cols,
681
692
  remove_cols_by_suffix=remove_cols_by_suffix,
682
693
  ignore_cols=ignore_cols,
@@ -694,6 +705,7 @@ def _get_diffs(
694
705
  mode=mode,
695
706
  rename_cols=rename_cols,
696
707
  retain_cols=retain_cols,
708
+ retain_rows=retain_rows,
697
709
  remove_cols=remove_cols,
698
710
  remove_cols_by_suffix=remove_cols_by_suffix,
699
711
  ignore_cols=ignore_cols,
@@ -719,6 +731,7 @@ def _get_single_diff(
719
731
  remove_cols: list | str = None,
720
732
  remove_cols_by_suffix='',
721
733
  retain_cols: list | str = None,
734
+ retain_rows: list | str = None,
722
735
  ignore_cols: list | str = None,
723
736
  name='data',
724
737
  linebreak='<br>',
@@ -752,6 +765,7 @@ def _get_single_diff(
752
765
 
753
766
  d = _set_uid(d, uid)
754
767
  d = _retain_cols(d, list_(retain_cols)) #depends on _set_uid()
768
+ d = _retain_rows(d, list_(retain_rows)) #depends on _set_uid()
755
769
  d = _ignore_cols(d, list_(ignore_cols))
756
770
 
757
771
  d = _create_working_values(d)
@@ -769,8 +783,10 @@ def _get_single_diff(
769
783
  d = _populate_diff_col(d) #depends on _get_row_col_diffs() and _get_val_diffs()
770
784
 
771
785
  d = _add_retained_cols(d)
772
- d = _add_diff_col(d)
786
+ d = _add_retained_rows(d)
773
787
  d = _add_uid_col(d)
788
+ d = _add_diff_col(d)
789
+ d = _reset_index(d)
774
790
  d = _apply_style(d)
775
791
 
776
792
  return d
@@ -822,7 +838,8 @@ class Diff:
822
838
  self.vals_changed: pd.Series = pd.Series(dtype='int')
823
839
 
824
840
  #helper attributes for internal use
825
- self._df_retained: pd.DataFrame
841
+ self._cols_retained: pd.DataFrame
842
+ self._rows_retained: pd.DataFrame
826
843
  self._cols_ignore: list[str]
827
844
  self._metadata_col_mapping: dict[str, str]
828
845
  self._mask_added: pd.DataFrame
@@ -863,9 +880,10 @@ class Diff:
863
880
  f'vals changed: {sum(self.vals_changed)}\n'
864
881
  )
865
882
  txt += (
866
- '>>>d.result\n'
867
- '>>>d.summary\n'
868
- '>>>d.details\n'
883
+ '>>>d.show()\n'
884
+ '>>>d.info()\n'
885
+ '>>>d.summary()\n'
886
+ '>>>d.details()\n'
869
887
  )
870
888
  txt += '----------------Diff object end----------------\n'
871
889
 
@@ -919,7 +937,8 @@ def _handle_edgecases(d: Diff) -> Diff:
919
937
  d._values = values
920
938
  d._style = style
921
939
 
922
- d._df_retained = pd.DataFrame()
940
+ d._cols_retained = pd.DataFrame()
941
+ d._rows_retained = pd.DataFrame()
923
942
  d._cols_ignore = []
924
943
  d._metadata_col_mapping = {}
925
944
  d._mask_added = pd.DataFrame(dtype='bool')
@@ -1117,17 +1136,41 @@ def _retain_cols(d: Diff, cols: list) -> Diff:
1117
1136
  """
1118
1137
 
1119
1138
  if not cols:
1120
- d._df_retained = pd.DataFrame()
1139
+ d._cols_retained = pd.DataFrame()
1121
1140
  return d
1122
1141
 
1123
1142
  cols_retain_old = d.old.columns.intersection(cols)
1124
1143
  cols_retain_new = d.new.columns.intersection(cols)
1125
1144
 
1126
- df_retained = d.old[cols_retain_old].copy()
1145
+ cols_retained = d.old[cols_retain_old].copy()
1127
1146
 
1128
1147
  d.old = d.old.drop(columns=cols_retain_old)
1129
1148
  d.new = d.new.drop(columns=cols_retain_new)
1130
- d._df_retained = df_retained
1149
+ d._cols_retained = cols_retained
1150
+
1151
+ return d
1152
+
1153
+
1154
+
1155
+ def _retain_rows(d: Diff, rows: list) -> Diff:
1156
+ """
1157
+ remove rows from both dfs before diffing,
1158
+ then readd the ones from the old df to
1159
+ the diff result later.
1160
+ """
1161
+
1162
+ if not rows:
1163
+ d._rows_retained = pd.DataFrame()
1164
+ return d
1165
+
1166
+ rows_retain_old = d.old.index.intersection(rows)
1167
+ rows_retain_new = d.new.index.intersection(rows)
1168
+
1169
+ rows_retained = d.old.loc[rows_retain_old].copy()
1170
+
1171
+ d.old = d.old.drop(index=rows_retain_old)
1172
+ d.new = d.new.drop(index=rows_retain_new)
1173
+ d._rows_retained = rows_retained
1131
1174
 
1132
1175
  return d
1133
1176
 
@@ -1158,7 +1201,7 @@ def _create_working_values(d: Diff) -> Diff:
1158
1201
  if d.mode == 'mix':
1159
1202
  rows_old_only = d.old.index.difference(d.new.index)
1160
1203
  cols_old_only = d.old.columns.difference(d.new.columns)
1161
- d._values = pd.concat([d.new, d.old.loc[:, cols_old_only]], axis=1)
1204
+ d._values = pd.concat([d.new, d.old.loc[:, cols_old_only]], axis='columns')
1162
1205
  d._values.loc[rows_old_only, :] = d.old.loc[rows_old_only, :]
1163
1206
 
1164
1207
  elif d.mode == 'old':
@@ -1266,11 +1309,11 @@ def _create_meta_cols(d: Diff) -> Diff:
1266
1309
 
1267
1310
  values = pd.concat(
1268
1311
  [values, df_meta],
1269
- axis=1,
1312
+ axis='columns',
1270
1313
  )
1271
1314
  style = pd.concat(
1272
1315
  [style, df_meta_style],
1273
- axis=1,
1316
+ axis='columns',
1274
1317
  )
1275
1318
 
1276
1319
  d._metadata_col_mapping = col_mapping
@@ -1396,9 +1439,9 @@ def _populate_val_styles(d: Diff) -> Diff:
1396
1439
 
1397
1440
  def _populate_diff_col(d: Diff) -> Diff:
1398
1441
 
1399
- sum_added = d._mask_added.sum(axis=1)
1400
- sum_removed = d._mask_removed.sum(axis=1)
1401
- sum_changed = d._mask_changed.sum(axis=1)
1442
+ sum_added = d._mask_added.sum(axis='columns')
1443
+ sum_removed = d._mask_removed.sum(axis='columns')
1444
+ sum_changed = d._mask_changed.sum(axis='columns')
1402
1445
 
1403
1446
  rows_added = sum_added[sum_added > 0].index
1404
1447
  rows_removed = sum_removed[sum_removed > 0].index
@@ -1450,21 +1493,49 @@ def _populate_meta_cols(d: Diff) -> Diff:
1450
1493
 
1451
1494
  def _add_retained_cols(d: Diff) -> Diff:
1452
1495
 
1453
- if d._df_retained.empty:
1496
+ if d._cols_retained.empty:
1454
1497
  return d
1455
1498
 
1456
- idx_shared = d._values.index.intersection(d._df_retained.index)
1457
- df_retained = d._df_retained.loc[idx_shared, :]
1458
- d._values = pd.concat([df_retained, d._values], axis=1)
1499
+ idx_shared = d._values.index.intersection(d._cols_retained.index)
1500
+ df_retained = d._cols_retained.loc[idx_shared, :]
1501
+ d._values = pd.concat([df_retained, d._values], axis='columns')
1502
+
1503
+ df_style = pd.DataFrame(
1504
+ data='',
1505
+ index=df_retained.index,
1506
+ columns=df_retained.columns,
1507
+ )
1508
+ d._style = pd.concat([df_style, d._style], axis='columns')
1459
1509
 
1460
1510
  return d
1461
1511
 
1462
1512
 
1463
1513
 
1464
- def _add_diff_col(d: Diff) -> Diff:
1465
- colname = ensure_unique_string('diff', d._values.columns)
1466
- d._diff_col.name = colname
1467
- d._values = pd.concat([d._diff_col, d._values], axis=1)
1514
+ def _add_retained_rows(d: Diff) -> Diff:
1515
+
1516
+ if d._rows_retained.empty:
1517
+ return d
1518
+
1519
+ cols_shared = d._values.columns.intersection(d._rows_retained.columns)
1520
+ df_retained = d._rows_retained.loc[:, cols_shared]
1521
+ cols_ordered = d._values.columns
1522
+ d._values = pd.concat([df_retained, d._values], axis='index')
1523
+ d._values = d._values[cols_ordered]
1524
+
1525
+ df_style = pd.DataFrame(
1526
+ data='',
1527
+ index=df_retained.index,
1528
+ columns=cols_shared,
1529
+ )
1530
+ d._style = pd.concat([df_style, d._style], axis='index')
1531
+
1532
+ blank_retained = pd.Series(
1533
+ '',
1534
+ index=df_retained.index,
1535
+ name=d._diff_col.name,
1536
+ )
1537
+ d._diff_col = pd.concat([blank_retained, d._diff_col], axis='index')
1538
+
1468
1539
  return d
1469
1540
 
1470
1541
 
@@ -1477,7 +1548,21 @@ def _add_uid_col(d: Diff) -> Diff:
1477
1548
  index=d._values.index,
1478
1549
  name=colname_uid,
1479
1550
  )
1480
- d._values = pd.concat([uid_col, d._values], axis=1)
1551
+ d._values = pd.concat([uid_col, d._values], axis='columns')
1552
+
1553
+ return d
1554
+
1555
+
1556
+
1557
+ def _add_diff_col(d: Diff) -> Diff:
1558
+ colname = ensure_unique_string('diff', d._values.columns)
1559
+ d._diff_col.name = colname
1560
+ d._values = pd.concat([d._diff_col, d._values], axis='columns')
1561
+ return d
1562
+
1563
+
1564
+
1565
+ def _reset_index(d: Diff) -> Diff:
1481
1566
 
1482
1567
  index_diff = d._values.index.difference(d._style.index)
1483
1568
  if index_diff.empty:
@@ -1489,7 +1574,7 @@ def _add_uid_col(d: Diff) -> Diff:
1489
1574
  'This should never happen. Please report this issue.'
1490
1575
  f'Index difference: {index_diff}'
1491
1576
  )
1492
- log(msg, 'dk.diffing._add_uid_col', d.verbosity)
1577
+ log(msg, 'dk.diffing._reset_index', d.verbosity)
1493
1578
 
1494
1579
  return d
1495
1580
 
@@ -1506,6 +1591,7 @@ def _apply_style(d: Diff) -> Diff:
1506
1591
  )
1507
1592
  log(msg, context, d.verbosity)
1508
1593
 
1594
+ d._style.fillna('', inplace=True)
1509
1595
  d.result = (
1510
1596
  d._values
1511
1597
  .style
@@ -136,6 +136,14 @@ def save(
136
136
  sheet_name='df',
137
137
  index=False,
138
138
  format_excel=True,
139
+ freeze_panes='B2',
140
+ hide_sheets=None,
141
+ hide_cols=None,
142
+ col_width_max=70,
143
+ col_width_padding=2,
144
+ align_vertical='top',
145
+ align_horizontal='left',
146
+ wrap_text=True,
139
147
  **kwargs,
140
148
  ):
141
149
  """
@@ -184,4 +192,15 @@ def save(
184
192
  )
185
193
 
186
194
  if format_excel:
187
- format(path, sheet=sheet_name)
195
+ format(
196
+ path,
197
+ sheet=sheet_name,
198
+ freeze_panes=freeze_panes,
199
+ hide_sheets=hide_sheets,
200
+ hide_cols=hide_cols,
201
+ col_width_max=col_width_max,
202
+ col_width_padding=col_width_padding,
203
+ align_vertical=align_vertical,
204
+ align_horizontal=align_horizontal,
205
+ wrap_text=wrap_text,
206
+ )
@@ -16,6 +16,7 @@ from .pandas import (
16
16
  flatten,
17
17
  stagger,
18
18
  transpose,
19
+ split_col,
19
20
  )
20
21
 
21
22
 
@@ -76,6 +77,9 @@ class DukitAccessor():
76
77
  uid: str = None,
77
78
  upper: int = None,
78
79
  lower: int = None,
80
+ start_at_day1: bool = True,
81
+ schedule: dict[int, str] = None,
82
+ filler: str = '.',
79
83
  linebreak: str = '<br>',
80
84
  verbosity: int = 3,
81
85
  ):
@@ -85,6 +89,9 @@ class DukitAccessor():
85
89
  uid=uid,
86
90
  upper=upper,
87
91
  lower=lower,
92
+ start_at_day1=start_at_day1,
93
+ schedule=schedule,
94
+ filler=filler,
88
95
  linebreak=linebreak,
89
96
  verbosity=verbosity,
90
97
  )
@@ -167,12 +174,37 @@ class DukitAccessor():
167
174
  return df_new
168
175
 
169
176
 
177
+ def split_col(
178
+ self,
179
+ col: str = 'uid',
180
+ sep: str = '::\n',
181
+ new1: str = None,
182
+ new2: str = 'label',
183
+ ):
184
+ df_new = split_col(
185
+ self.df,
186
+ col=col,
187
+ sep=sep,
188
+ new1=new1,
189
+ new2=new2,
190
+ )
191
+ return df_new
192
+
193
+
170
194
  def save(
171
195
  self,
172
196
  path: str,
173
197
  sheet_name='df',
174
198
  index=False,
175
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,
176
208
  **kwargs,
177
209
  ):
178
210
  save(
@@ -181,6 +213,14 @@ class DukitAccessor():
181
213
  sheet_name=sheet_name,
182
214
  index=index,
183
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,
184
224
  **kwargs,
185
225
  )
186
226
  return None
@@ -4,7 +4,7 @@ import datetime
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
 
7
- from .utils import log, GREEN_LIGHT
7
+ from .utils import log, ensure_unique_string, GREEN_LIGHT
8
8
  from .typing import date_
9
9
 
10
10
 
@@ -183,8 +183,8 @@ def get_df() -> pd.DataFrame:
183
183
 
184
184
 
185
185
 
186
- def get_dfs():
187
- df1 = pd.DataFrame({
186
+ def get_df_patients() -> pd.DataFrame:
187
+ patients = pd.DataFrame({
188
188
  'id': [
189
189
  10001,
190
190
  10002,
@@ -204,7 +204,12 @@ def get_dfs():
204
204
  40,
205
205
  ],
206
206
  })
207
- df2 = pd.DataFrame({
207
+ return patients
208
+
209
+
210
+
211
+ def get_df_medications() -> pd.DataFrame:
212
+ medications = pd.DataFrame({
208
213
  'id': [
209
214
  10002,
210
215
  20001,
@@ -238,7 +243,55 @@ def get_dfs():
238
243
  'mg',
239
244
  ]
240
245
  })
241
- return df1, df2
246
+ return medications
247
+
248
+
249
+ def get_df_old() -> pd.DataFrame:
250
+
251
+ old = pd.DataFrame(
252
+ columns=['a', 'b', 'c'],
253
+ index=['x', 'y', 'z', ],
254
+ )
255
+
256
+ old.insert(0, 'uid', old.index)
257
+
258
+ old.loc['x', 'a'] = 1
259
+ old.loc['x', 'b'] = 1
260
+ old.loc['x', 'c'] = 1
261
+
262
+ old.loc['y', 'a'] = 2
263
+ old.loc['y', 'b'] = 2
264
+ old.loc['y', 'c'] = 2
265
+
266
+ old.loc['z', 'a'] = 3
267
+ old.loc['z', 'b'] = None
268
+ old.loc['z', 'c'] = 3
269
+
270
+ return old
271
+
272
+
273
+ def get_df_new() -> pd.DataFrame:
274
+
275
+ new = pd.DataFrame(
276
+ columns=['d', 'b', 'a'],
277
+ index=['y', 'x2', 'z', ],
278
+ )
279
+
280
+ new.insert(0, 'uid', new.index)
281
+
282
+ new.loc['y', 'd'] = 2
283
+ new.loc['y', 'b'] = 2
284
+ new.loc['y', 'a'] = 0
285
+
286
+ new.loc['x2', 'd'] = 1
287
+ new.loc['x2', 'b'] = 1
288
+ new.loc['x2', 'a'] = 1
289
+
290
+ new.loc['z', 'd'] = 3
291
+ new.loc['z', 'b'] = 3
292
+ new.loc['z', 'a'] = pd.NA
293
+
294
+ return new
242
295
 
243
296
 
244
297
 
@@ -342,6 +395,7 @@ def date_table(
342
395
  lower: int = None,
343
396
  start_at_day1: bool = True,
344
397
  schedule: dict[int, str] = None,
398
+ filler: str = '.',
345
399
  linebreak: str = '\n',
346
400
  verbosity: int = 3,
347
401
  ) -> pd.DataFrame | pd.io.formats.style.Styler:
@@ -359,19 +413,17 @@ def date_table(
359
413
  df = df.copy()
360
414
  if uid:
361
415
  df.index = df[uid]
362
- reference = df[reference_col].apply(date_)
363
-
364
- df = (
365
- df
366
- .copy()
367
- .map(date_)
368
- .convert_dtypes()
369
- )
416
+ df = df.map(date_).convert_dtypes()
370
417
 
371
418
  for col in df.columns:
372
419
  if df[col].isna().all():
373
- df.drop(columns=col, inplace=True)
420
+ if col == reference_col:
421
+ msg = f'WARNING: no dates found in reference col "{reference_col}"'
422
+ log(msg, 'dk.date_delta', verbosity)
423
+ else:
424
+ df.drop(columns=col, inplace=True)
374
425
 
426
+ reference = df[reference_col].apply(date_).apply(_fix_nas)
375
427
 
376
428
  blanks = {col: pd.NaT for col in df.columns}
377
429
  deltas = pd.DataFrame(
@@ -393,11 +445,10 @@ def date_table(
393
445
 
394
446
  deltas = deltas.T
395
447
 
396
-
397
448
  if upper is None:
398
- upper = int(deltas.max().max()) + 1
449
+ upper = int(deltas.fillna(0).max().max()) + 1
399
450
  if lower is None:
400
- lower = int(deltas.min().min())
451
+ lower = int(deltas.fillna(0).min().min())
401
452
 
402
453
  days = list(range(lower, upper + 1))
403
454
  if start_at_day1:
@@ -427,7 +478,7 @@ def date_table(
427
478
  if start_at_day1:
428
479
  events = _shift_day0(events)
429
480
 
430
- merged = days_df.copy().merge(events, on='days', how='left').fillna('')
481
+ merged = days_df.copy().merge(events, on='days', how='left').fillna(filler)
431
482
  collapsed = collapse(
432
483
  merged,
433
484
  on='days',
@@ -439,6 +490,23 @@ def date_table(
439
490
  how='left',
440
491
  )
441
492
 
493
+ cols_records = [
494
+ col for col
495
+ in df_timeline.columns
496
+ if col not in ('_meta', 'days', 'planned')
497
+ ]
498
+ df_timeline.insert(
499
+ loc=0,
500
+ column='_meta',
501
+ value=''
502
+ )
503
+ has_event = df_timeline[cols_records] != filler
504
+ records_per_row = has_event.sum(axis='columns')
505
+ rows_with_records = records_per_row > 0
506
+ summary = 'records: ' + records_per_row.astype(str)
507
+ df_timeline.loc[rows_with_records, '_meta'] = summary
508
+ df_timeline['_meta'] = df_timeline['_meta'].fillna('')
509
+
442
510
  if schedule is not None:
443
511
  df_timeline = _highlight_schedule(df_timeline, schedule)
444
512
 
@@ -819,3 +887,39 @@ def transpose(
819
887
  df.reset_index(drop=True, inplace=True)
820
888
 
821
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
@@ -1413,8 +1413,8 @@ class GetNotEquals(Symbol):
1413
1413
 
1414
1414
  class GetContains(Symbol):
1415
1415
  """
1416
- get cols/rows/vals if
1417
- they contain an arg.
1416
+ get cols/rows/vals if they
1417
+ contain a given string.
1418
1418
 
1419
1419
  Examples
1420
1420
  --------
@@ -1495,6 +1495,184 @@ class GetContains(Symbol):
1495
1495
 
1496
1496
 
1497
1497
 
1498
+ class GetStartsWith(Symbol):
1499
+ """
1500
+ get cols/rows/vals if they
1501
+ start with a given string.
1502
+
1503
+ Examples
1504
+ --------
1505
+ >>> df.dk.qs(r'name :start(john)')
1506
+ """
1507
+
1508
+ #symbol attributes
1509
+ name = 'GetStartsWith'
1510
+ category = 'getter'
1511
+ regex = (
1512
+ r':startswith',
1513
+ r':starts',
1514
+ r':start',
1515
+ )
1516
+
1517
+ #used to validate the current op
1518
+ connectors_allowed = {
1519
+ 'new': 'start a new op',
1520
+ 'and': 'combine getter selection with current selection using logical AND',
1521
+ 'or': 'combine getter selection with current selection using logical OR',
1522
+ }
1523
+ scopes_allowed = {
1524
+ 'cols': 'get or set cols/headers',
1525
+ 'rows': 'get or set rows/index',
1526
+ 'vals': 'get or set vals within current row and col selection',
1527
+ }
1528
+ flags_allowed = {
1529
+ 'negate': 'negate the condition',
1530
+ 'regex': 'parse arg as regex',
1531
+
1532
+ 'index': 'condition is applied to index instead of vals',
1533
+ 'any': 'condition must apply to any args',
1534
+ 'all': 'condition must apply to all args',
1535
+ 'allcols': 'get rows where condition applies in all selected cols',
1536
+
1537
+ 'strict': 'strict type comparison/conversion',
1538
+ }
1539
+ args_allowed = {}
1540
+ args_min = 1
1541
+ args_max = 1_000_000
1542
+
1543
+
1544
+ def parse(self, q: Query) -> Query:
1545
+ q = _preparse_for_getter(q)
1546
+ q = _parse_op_symbol(self, q)
1547
+ return q
1548
+
1549
+
1550
+ def run(self, q: Query) -> Query:
1551
+ if self.scope == 'cols':
1552
+ q = _get_cols(self, q)
1553
+ elif self.scope == 'rows':
1554
+ q = _get_rows(self, q)
1555
+ elif self.scope == 'vals':
1556
+ q = _get_vals(self, q)
1557
+ return q
1558
+
1559
+
1560
+ def getter(
1561
+ self,
1562
+ series: pd.Series,
1563
+ mask: pd.Series[bool],
1564
+ arg: str,
1565
+ q: Query,
1566
+ ) -> pd.Series[bool]:
1567
+
1568
+ series = series.astype('string')
1569
+
1570
+ if 'regex' in self.flags:
1571
+ mask = series.str.match(arg)
1572
+
1573
+ elif 'strict' in self.flags:
1574
+ mask = series.str.startswith(arg)
1575
+
1576
+ else:
1577
+ series = series.str.lower()
1578
+ arg = arg.lower()
1579
+ mask = series.str.startswith(arg)
1580
+
1581
+ return mask
1582
+
1583
+
1584
+
1585
+
1586
+ class GetEndsWith(Symbol):
1587
+ """
1588
+ get cols/rows/vals if they
1589
+ end with a given string.
1590
+
1591
+ Examples
1592
+ --------
1593
+ >>> df.dk.qs(r'name :end(doe)')
1594
+ """
1595
+
1596
+ #symbol attributes
1597
+ name = 'GetEndsWith'
1598
+ category = 'getter'
1599
+ regex = (
1600
+ r':endswith',
1601
+ r':ends',
1602
+ r':end',
1603
+ )
1604
+
1605
+ #used to validate the current op
1606
+ connectors_allowed = {
1607
+ 'new': 'start a new op',
1608
+ 'and': 'combine getter selection with current selection using logical AND',
1609
+ 'or': 'combine getter selection with current selection using logical OR',
1610
+ }
1611
+ scopes_allowed = {
1612
+ 'cols': 'get or set cols/headers',
1613
+ 'rows': 'get or set rows/index',
1614
+ 'vals': 'get or set vals within current row and col selection',
1615
+ }
1616
+ flags_allowed = {
1617
+ 'negate': 'negate the condition',
1618
+ 'regex': 'parse arg as regex',
1619
+
1620
+ 'index': 'condition is applied to index instead of vals',
1621
+ 'any': 'condition must apply to any args',
1622
+ 'all': 'condition must apply to all args',
1623
+ 'allcols': 'get rows where condition applies in all selected cols',
1624
+
1625
+ 'strict': 'strict type comparison/conversion',
1626
+ }
1627
+ args_allowed = {}
1628
+ args_min = 1
1629
+ args_max = 1_000_000
1630
+
1631
+
1632
+ def parse(self, q: Query) -> Query:
1633
+ q = _preparse_for_getter(q)
1634
+ q = _parse_op_symbol(self, q)
1635
+ return q
1636
+
1637
+
1638
+ def run(self, q: Query) -> Query:
1639
+ if self.scope == 'cols':
1640
+ q = _get_cols(self, q)
1641
+ elif self.scope == 'rows':
1642
+ q = _get_rows(self, q)
1643
+ elif self.scope == 'vals':
1644
+ q = _get_vals(self, q)
1645
+ return q
1646
+
1647
+
1648
+ def getter(
1649
+ self,
1650
+ series: pd.Series,
1651
+ mask: pd.Series[bool],
1652
+ arg: str,
1653
+ q: Query,
1654
+ ) -> pd.Series[bool]:
1655
+
1656
+ series = series.astype('string')
1657
+
1658
+ if 'regex' in self.flags:
1659
+ if not arg.endswith('$'):
1660
+ arg = arg + '$'
1661
+ mask = series.str.contains(arg)
1662
+
1663
+ elif 'strict' in self.flags:
1664
+ mask = series.str.endswith(arg)
1665
+
1666
+ else:
1667
+ series = series.str.lower()
1668
+ arg = arg.lower()
1669
+ mask = series.str.endswith(arg)
1670
+
1671
+ return mask
1672
+
1673
+
1674
+
1675
+
1498
1676
  class GetGreaterEqual(Symbol):
1499
1677
  """
1500
1678
  get cols/rows/vals if
@@ -84,6 +84,10 @@ VALUES_NA = (
84
84
  'blank',
85
85
  'empty',
86
86
  )
87
+ VALUES_NA_STRICT = (
88
+ '<na>',
89
+ 'none',
90
+ )
87
91
  VALUES_NK = (
88
92
  'unk',
89
93
  'unknown',
@@ -487,6 +491,10 @@ def type_(x) -> str:
487
491
  return 'int'
488
492
  elif isinstance(x, TYPES_FLOAT):
489
493
  return 'float'
494
+ elif pd.isna(x):
495
+ return 'na'
496
+ elif x == '':
497
+ return 'str'
490
498
  elif datetime_(x) is not pd.NaT:
491
499
  x = str(x).strip()
492
500
  if re.fullmatch(r'\d*', x):
@@ -504,7 +512,7 @@ def type_(x) -> str:
504
512
  return 'int'
505
513
  elif re.fullmatch(r'-?\d+[\.,]\d+', x):
506
514
  return 'float'
507
- elif str(x).lower() in VALUES_NA:
515
+ elif str(x).lower() in VALUES_NA_STRICT:
508
516
  return 'na'
509
517
  else:
510
518
  try:
@@ -513,9 +521,6 @@ def type_(x) -> str:
513
521
  except Exception:
514
522
  return 'str'
515
523
 
516
- elif pd.isna(x):
517
- return 'na'
518
-
519
524
  else:
520
525
  return type(x).__name__
521
526
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes