dukit 0.1.0__py3-none-any.whl

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.
dukit/pandas.py ADDED
@@ -0,0 +1,563 @@
1
+
2
+ import pandas as pd
3
+ import numpy as np
4
+ import datetime
5
+ import copy
6
+ from .utils import log
7
+ from .typing import date_
8
+
9
+ def get_df() -> pd.DataFrame:
10
+ """
11
+ Returns a small sample dataframe containing very messy fake medical data.
12
+ """
13
+ data = {
14
+ 'ID': [
15
+ 10001,
16
+ 10002,
17
+ 10003,
18
+ 20001,
19
+ 20002,
20
+ 20003,
21
+ 30001,
22
+ 30002,
23
+ 30003,
24
+ 30004,
25
+ 30005,
26
+ ],
27
+ 'name': [
28
+ 'John Doe',
29
+ 'Jane Smith',
30
+ 'Alice Johnson',
31
+ 'Bob Brown',
32
+ 'eva white',
33
+ 'Frank miller',
34
+ 'Grace TAYLOR',
35
+ 'Harry Clark',
36
+ 'IVY GREEN',
37
+ 'JAck Williams',
38
+ 'john Doe',
39
+ ],
40
+ 'date of birth': [
41
+ pd.to_datetime('1995-01-02'),
42
+ datetime.datetime(1990, 9, 14),
43
+ '1985.08.23',
44
+ '19800406',
45
+ '05-11-2007',
46
+ pd.to_datetime('06-30-1983').date(),
47
+ datetime.datetime(1975, 5, 28).date(),
48
+ '1960Mar08',
49
+ '1955-Jan-09',
50
+ '1950 Sep 10',
51
+ '1945 October 11',
52
+ ],
53
+ 'age': [
54
+ -25,
55
+ '30',
56
+ np.nan,
57
+ pd.NaT,
58
+ '40.0',
59
+ 'forty-five',
60
+ 'nan',
61
+ 'unk',
62
+ '',
63
+ 'unknown',
64
+ 35,
65
+ ],
66
+ 'gender': [
67
+ 'M',
68
+ 'F',
69
+ 'Female',
70
+ 'Male',
71
+ 'Other',
72
+ 'm',
73
+ 'ff',
74
+ 'NaN',
75
+ None,
76
+ 'Mal',
77
+ 'female',
78
+ ],
79
+ 'height': [
80
+ 170,
81
+ '175.5cm',
82
+ None,
83
+ '280',
84
+ 'NULL',
85
+ '185',
86
+ '1',
87
+ '6ft 1in',
88
+ -10,
89
+ '',
90
+ 200,
91
+ ],
92
+ 'weight': [
93
+ 70.2,
94
+ '68',
95
+ '72.5lb',
96
+ 'na',
97
+ '',
98
+ '75kg',
99
+ None,
100
+ '80.3',
101
+ '130lbs',
102
+ '1e2',
103
+ -65,
104
+ ],
105
+ 'bp systole': [
106
+ ' 20 ',
107
+ 130,
108
+ '<NA>',
109
+ -np.inf,
110
+ '135mmhg',
111
+ '125.5',
112
+ 'NAN',
113
+ '122,3',
114
+ '',
115
+ 130,
116
+ '45',
117
+ ],
118
+ 'bp diastole': [
119
+ 80,
120
+ '85',
121
+ 'N/A',
122
+ '90mmHg',
123
+ np.nan,
124
+ '75',
125
+ 'NaN',
126
+ pd.NA,
127
+ '95',
128
+ '0',
129
+ 'None ',
130
+ ],
131
+ 'cholesterol': [
132
+ 'Normal',
133
+ 'Highe',
134
+ 'NaN',
135
+ 'GOOD',
136
+ 'n.a.',
137
+ 'High',
138
+ 'Normal',
139
+ 'n/a',
140
+ 'high',
141
+ '',
142
+ 'Normal',
143
+ ],
144
+ 'diabetes': [
145
+ False,
146
+ 'true',
147
+ 'N/A',
148
+ 0,
149
+ '1',
150
+ 'Yes',
151
+ 'NO',
152
+ None,
153
+ 'NaN',
154
+ 'n',
155
+ True,
156
+ ],
157
+ 'dose': [
158
+ '10kg',
159
+ 'NaN',
160
+ '15 mg once a day',
161
+ '20mg',
162
+ '20 Mg',
163
+ '25g',
164
+ 'NaN',
165
+ None,
166
+ '30 MG',
167
+ ' 35 ',
168
+ '40ml',
169
+ ],
170
+ }
171
+ df = pd.DataFrame(data).convert_dtypes()
172
+ df.columns = df.columns.to_series().convert_dtypes()
173
+ df.index = df.index.to_series().convert_dtypes()
174
+ return df
175
+
176
+
177
+
178
+ def get_dfs():
179
+ df1 = pd.DataFrame({
180
+ 'id': [
181
+ 10001,
182
+ 10002,
183
+ 20001,
184
+ 30001,
185
+ ],
186
+ 'name': [
187
+ 'John Doe',
188
+ 'Jane Smith',
189
+ 'Alice Johnson',
190
+ 'Bob Brown',
191
+ ],
192
+ 'age': [
193
+ 25,
194
+ 30,
195
+ 35,
196
+ 40,
197
+ ],
198
+ })
199
+ df2 = pd.DataFrame({
200
+ 'id': [
201
+ 10002,
202
+ 20001,
203
+ 20001,
204
+ 30001,
205
+ 30001,
206
+ 30001,
207
+ ],
208
+ 'medication': [
209
+ 'Aspirin',
210
+ 'Ibuprofen',
211
+ 'Paracetamol',
212
+ 'Amoxicillin',
213
+ 'Ciprofloxacin',
214
+ 'Metformin',
215
+ ],
216
+ 'dose': [
217
+ 100,
218
+ 200,
219
+ pd.NA,
220
+ 250,
221
+ 500,
222
+ 1000,
223
+ ],
224
+ 'unit': [
225
+ 'mg',
226
+ 'mg',
227
+ '',
228
+ 'mg',
229
+ 'ml',
230
+ 'mg',
231
+ ]
232
+ })
233
+ return df1, df2
234
+
235
+
236
+
237
+
238
+ def deduplicate(obj, name='object', verbosity=3):
239
+ """
240
+ Deduplicate entries in object which can be converted
241
+ to pandas Series by appending consecutive numbers.
242
+ Note that the entries are converted to strings in the process.
243
+ """
244
+
245
+ obj = copy.deepcopy(obj)
246
+ class_orig = obj.__class__
247
+ obj = _to_series(obj)
248
+
249
+ #Values can only be deduplicated if index is unique
250
+ if not obj.index.is_unique:
251
+ msg = (
252
+ f'info: duplicates found in index of {name}.'
253
+ ' deduplicating by appending consecutive numbers.'
254
+ )
255
+ log(msg, 'dk.pandas.deduplicate', verbosity)
256
+ obj.index = _deduplicate(_to_series(obj.index))
257
+
258
+ rounds = 0
259
+ while not obj.is_unique:
260
+ if rounds == 0:
261
+ msg = (
262
+ f'debug: duplicates found in {name}.'
263
+ ' deduplicating by appending consecutive numbers.'
264
+ )
265
+ else:
266
+ msg = (
267
+ f'debug: duplicates still found in {name} after'
268
+ f' {rounds} deduplication rounds.'
269
+ ' deduplicating again by appending consecutive numbers.'
270
+ )
271
+ log(msg, 'dk.pandas.deduplicate', verbosity)
272
+ obj = _deduplicate(obj)
273
+ rounds += 1
274
+
275
+ if not isinstance(obj, class_orig):
276
+ try:
277
+ obj = class_orig(obj)
278
+ except Exception as e:
279
+ msg = (
280
+ 'Error: could not convert deduplicated object'
281
+ f' back to original type {class_orig}: {e}'
282
+ )
283
+ log(msg, 'dk.pandas.deduplicate', verbosity)
284
+
285
+ return obj
286
+
287
+
288
+ def _to_series(obj, verbosity=3):
289
+ try:
290
+ obj = pd.Series(obj, dtype=str)
291
+ except Exception as e:
292
+ msg = (
293
+ 'Error: could not convert input of type'
294
+ f' {type(obj)} to Series: {e}'
295
+ )
296
+ log(msg, 'dk.pandas._to_series', verbosity)
297
+ return obj
298
+
299
+
300
+ def _deduplicate(series):
301
+ cumulative_count = series.groupby(series).cumcount()
302
+ duplicates_mask = series.index[cumulative_count > 0]
303
+ duplicates = series[duplicates_mask]
304
+ duplicates_new = (
305
+ duplicates.astype(str)
306
+ + '_'
307
+ + cumulative_count[duplicates_mask].astype(str)
308
+ )
309
+ series[duplicates_mask] = duplicates_new
310
+ return series
311
+
312
+
313
+
314
+
315
+ def flatten(
316
+ df: pd.DataFrame,
317
+ on: str,
318
+ template='{colname} #{counter}',
319
+ ):
320
+
321
+ #aggregate repeating rows into lists
322
+ df = df.groupby(on).agg(list)
323
+
324
+ #create new cols from lists
325
+ cols_new = []
326
+ for col in df.columns:
327
+ n_max = df[col].apply(lambda x: len(x)).max()
328
+ cols_flat = [
329
+ template.format(counter=i + 1, colname=col)
330
+ for i
331
+ in range(n_max)
332
+ ]
333
+ split = pd.DataFrame(df[col].to_list(), columns=cols_flat)
334
+ split.index = df.index
335
+ df = df.merge(
336
+ split,
337
+ left_index=True,
338
+ right_index=True,
339
+ how='left',
340
+ )
341
+ cols_new += cols_flat
342
+
343
+ df = df[cols_new]
344
+ df.insert(0, on, df.index)
345
+ df.reset_index(drop=True, inplace=True)
346
+
347
+ return df
348
+
349
+
350
+
351
+ def stagger(
352
+ df: pd.DataFrame,
353
+ on: str,
354
+ template='#{counter} {colname}',
355
+ separator_col='#{counter}'
356
+ ):
357
+
358
+ duplicates_max = df[on].value_counts().max()
359
+ df = df.groupby(on).agg(list)
360
+ cols = df.columns.tolist()
361
+ df_new = pd.DataFrame(
362
+ df.index,
363
+ index=df.index,
364
+ )
365
+
366
+ for i in range(duplicates_max):
367
+ if separator_col:
368
+ separator_vals = pd.Series(
369
+ '',
370
+ index=df.index,
371
+ name=separator_col.format(counter=i + 1),
372
+ )
373
+ df_new = pd.concat([df_new, separator_vals], axis=1)
374
+ for col in cols:
375
+ if col == on:
376
+ continue
377
+ col_new = df[col].apply(lambda x: _get_from_list(x, i))
378
+ col_new.name = template.format(counter=i + 1, colname=col)
379
+ df_new = pd.concat([df_new, col_new], axis=1)
380
+
381
+ df_new.reset_index(drop=True, inplace=True)
382
+
383
+ return df_new
384
+
385
+
386
+ def _get_from_list(x, i):
387
+ if len(x) > i:
388
+ return x[i]
389
+ else:
390
+ return None
391
+
392
+
393
+
394
+ def embed(
395
+ df: pd.DataFrame,
396
+ on: str,
397
+ colname='',
398
+ template='{colname} #{counter}',
399
+ line_start='',
400
+ separator=':',
401
+ spacer='\u00A0', #non-breaking space
402
+ line_stop='\n',
403
+ ):
404
+
405
+ combined_cols_str = pd.DataFrame({
406
+ on: df[on],
407
+ colname: ''
408
+ })
409
+
410
+ len_max_cols = (
411
+ df
412
+ .columns
413
+ .to_series()
414
+ .astype('string')
415
+ .str
416
+ .len()
417
+ .max()
418
+ )
419
+ for col in df.columns:
420
+ if col == on:
421
+ continue
422
+ len_spacer = (
423
+ len_max_cols
424
+ - len(str(col))
425
+ + 1
426
+ )
427
+ combined_cols_str[colname] += (
428
+ line_start
429
+ + str(col)
430
+ + separator
431
+ + spacer * len_spacer
432
+ + df[col].apply(str)
433
+ + line_stop
434
+ )
435
+
436
+ df_new = flatten(
437
+ combined_cols_str,
438
+ on=on,
439
+ template=template,
440
+ )
441
+
442
+ return df_new
443
+
444
+
445
+
446
+ def collapse(
447
+ df: pd.DataFrame,
448
+ on: str,
449
+ template='{colname}',
450
+ line_start='#',
451
+ line_stop='\n',
452
+ ):
453
+ df = df.groupby(on).agg(list)
454
+ df = df.map(lambda x: _to_lines(x, line_start, line_stop))
455
+ df.columns = [template.format(colname=col) for col in df.columns]
456
+ df.insert(0, on, df.index)
457
+ df.reset_index(drop=True, inplace=True)
458
+ return df
459
+
460
+
461
+ def _to_lines(
462
+ x,
463
+ line_start='#',
464
+ line_stop='\n',
465
+ ):
466
+ if not isinstance(x, list):
467
+ return x
468
+ if len(x) == 1:
469
+ return x[0]
470
+ else:
471
+ x_str = ''
472
+ for i, item in enumerate(x):
473
+ x_str += f'{line_start}{i + 1}: {item}{line_stop}'
474
+ return x_str
475
+
476
+
477
+
478
+ def transpose(
479
+ df: pd.DataFrame,
480
+ header='uid',
481
+ ) -> pd.DataFrame:
482
+
483
+ cols_old = df.columns
484
+ cols_new = df[header].values
485
+
486
+ df = df.T
487
+ df.columns = cols_new
488
+ df.insert(0, header, cols_old)
489
+ df.drop(header, axis=0, inplace=True)
490
+ df.reset_index(drop=True, inplace=True)
491
+
492
+ return df
493
+
494
+
495
+
496
+ def date_delta(
497
+ df,
498
+ reference_date=None,
499
+ reference_col=None,
500
+ verbosity=3,
501
+ ):
502
+ """
503
+ Calculates the number of days
504
+ between a reference date or col
505
+ and all other cols in the df.
506
+ """
507
+
508
+ df = df.copy().map(date_).convert_dtypes()
509
+
510
+ for col in df.columns:
511
+ if df[col].isna().all():
512
+ df.drop(columns=col, inplace=True)
513
+
514
+
515
+ if reference_date is None and reference_col is None:
516
+ msg = 'ERROR: no reference date or column provided'
517
+ log(msg, 'dk.date_delta', verbosity)
518
+ return df
519
+
520
+ if reference_date is not None and reference_col is not None:
521
+ msg = 'ERROR: both reference date and column provided'
522
+ log(msg, 'dk.date_delta', verbosity)
523
+ return df
524
+
525
+ if 'reference_date' in df.columns:
526
+ msg = 'WARNING: column "reference_date" already exists, overwriting'
527
+ log(msg, 'dk.date_delta', verbosity)
528
+ if reference_date is not None:
529
+ df['reference_date'] = date_(reference_date)
530
+ reference_col = str(reference_date)
531
+ else:
532
+ df['reference_date'] = df[reference_col].apply(date_)
533
+
534
+ cols_reorder = ['reference_date']
535
+ cols_reorder += [
536
+ col
537
+ for col
538
+ in df.columns
539
+ if col != 'reference_date'
540
+ ]
541
+ df = df[cols_reorder]
542
+
543
+ for col in df.columns:
544
+ if col == 'reference_date':
545
+ continue
546
+
547
+ name = f'days from<br>{reference_col}<br>to<br>{col}'
548
+ if name in df.columns:
549
+ log(f'WARNING: column "{name}" already exists, overwriting',
550
+ 'dk.date_delta', verbosity)
551
+
552
+ #difference only works with pd.NA,
553
+ #but x.days only works with pd.NaT.
554
+ #_fix_nas converts accordingly
555
+ col_formatted = df[col].apply(date_).apply(_fix_nas)
556
+ col_diff = col_formatted - df['reference_date']
557
+ df[name] = col_diff.fillna(pd.NaT).apply(lambda x: x.days)
558
+
559
+ return df
560
+
561
+
562
+ def _fix_nas(x):
563
+ return pd.NA if x is pd.NaT else x
dukit/py.typed ADDED
File without changes
@@ -0,0 +1,61 @@
1
+
2
+ import pandas as pd
3
+ from .engine import Query
4
+ from .symbols import all as symbols_all
5
+ from . import (
6
+ engine,
7
+ symbols,
8
+ )
9
+
10
+ def q(
11
+ df: pd.DataFrame,
12
+ code: str = '',
13
+ verbosity: int = 3,
14
+ ) -> Query:
15
+ query = Query(
16
+ df,
17
+ code,
18
+ symbols_all,
19
+ symbols.QueryStart(),
20
+ symbols.QueryStop(),
21
+ verbosity,
22
+ )
23
+ return query
24
+
25
+
26
+ def qr(
27
+ df: pd.DataFrame,
28
+ code: str = '',
29
+ verbosity: int = 3,
30
+ ) -> Query:
31
+ query_object = (
32
+ q(df, code, verbosity)
33
+ .scan(verbosity)
34
+ .parse(verbosity)
35
+ .run(verbosity)
36
+ )
37
+ return query_object
38
+
39
+
40
+ def qs(
41
+ df: pd.DataFrame,
42
+ code: str = '',
43
+ verbosity: int = 3,
44
+ ) -> pd.DataFrame | pd.io.formats.style.Styler:
45
+ result = (
46
+ q(df, code, verbosity)
47
+ .scan(verbosity)
48
+ .parse(verbosity)
49
+ .run(verbosity)
50
+ .show()
51
+ )
52
+ return result
53
+
54
+
55
+ __all__ = (
56
+ 'engine',
57
+ 'symbols',
58
+ 'q',
59
+ 'qr',
60
+ 'qs',
61
+ )