mspu 0.0.1__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.
- mspu/__init__.py +6 -0
- mspu/data/__init__.py +5 -0
- mspu/data/rand_df.py +346 -0
- mspu/datetime/__init__.py +0 -0
- mspu/datetime/utils.py +0 -0
- mspu/io/__init__.py +0 -0
- mspu/io/file.py +0 -0
- mspu/pandas/__init__.py +10 -0
- mspu/pandas/datetime.py +262 -0
- mspu/pandas/parquet.py +36 -0
- mspu/pandas/utils.py +196 -0
- mspu/polars/__init__.py +17 -0
- mspu/polars/utils.py +142 -0
- mspu/security/crypt.py +80 -0
- mspu-0.0.1.dist-info/METADATA +32 -0
- mspu-0.0.1.dist-info/RECORD +18 -0
- mspu-0.0.1.dist-info/WHEEL +4 -0
- mspu-0.0.1.dist-info/licenses/LICENSE +21 -0
mspu/__init__.py
ADDED
mspu/data/__init__.py
ADDED
mspu/data/rand_df.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import string
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def gen_rand_strs(
|
|
8
|
+
rng: np.random.Generator,
|
|
9
|
+
str_cnt: int,
|
|
10
|
+
str_len: tuple[int, int],
|
|
11
|
+
str_chars: list[str],
|
|
12
|
+
) -> list[str]:
|
|
13
|
+
str_lens = rng.integers(
|
|
14
|
+
low=str_len[0], high=str_len[1], size=str_cnt, endpoint=True
|
|
15
|
+
)
|
|
16
|
+
rand_strs = [''.join(rng.choice(str_chars, size=str_len)) for str_len in str_lens]
|
|
17
|
+
return rand_strs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gen_str_vals(
|
|
21
|
+
size: int,
|
|
22
|
+
rng: np.random.Generator,
|
|
23
|
+
str_cnt: int = None,
|
|
24
|
+
str_len: int | tuple[int, int] = None,
|
|
25
|
+
str_chars: list[str] = None,
|
|
26
|
+
col_strs: list[str] = None,
|
|
27
|
+
) -> np.ndarray:
|
|
28
|
+
if str_cnt is None:
|
|
29
|
+
str_cnt = 10
|
|
30
|
+
if str_len is None:
|
|
31
|
+
str_len = (5, 5)
|
|
32
|
+
elif isinstance(str_len, int):
|
|
33
|
+
str_len = (str_len, str_len)
|
|
34
|
+
if col_strs is None:
|
|
35
|
+
if str_chars is None:
|
|
36
|
+
str_chars = [c for c in string.ascii_letters + string.digits]
|
|
37
|
+
col_strs = gen_rand_strs(rng, str_cnt, str_len, str_chars)
|
|
38
|
+
val = rng.choice(col_strs, size=size)
|
|
39
|
+
return val
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def gen_ts_vals(
|
|
43
|
+
size: int,
|
|
44
|
+
rng: np.random.Generator,
|
|
45
|
+
start_date: str = None,
|
|
46
|
+
end_date: str = None,
|
|
47
|
+
freq: str = None,
|
|
48
|
+
random: bool = False,
|
|
49
|
+
) -> np.ndarray:
|
|
50
|
+
if start_date is None:
|
|
51
|
+
start_date = '2024-01-01'
|
|
52
|
+
if end_date is None:
|
|
53
|
+
end_date = '2025-01-01'
|
|
54
|
+
if freq is None:
|
|
55
|
+
freq = 'D'
|
|
56
|
+
if random is None:
|
|
57
|
+
random = False
|
|
58
|
+
val = pd.date_range(start_date, end_date, freq=freq, inclusive='left')[:size]
|
|
59
|
+
if random:
|
|
60
|
+
val = rng.choice(val, size=size)
|
|
61
|
+
return val
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def gen_num_vals(
|
|
65
|
+
size: int,
|
|
66
|
+
rng: np.random.Generator,
|
|
67
|
+
low: int | float = None,
|
|
68
|
+
high: int | float = None,
|
|
69
|
+
dtype: str = None,
|
|
70
|
+
) -> np.ndarray:
|
|
71
|
+
if low is None:
|
|
72
|
+
low = 0
|
|
73
|
+
if high is None:
|
|
74
|
+
high = 2
|
|
75
|
+
func = rng.integers if dtype[0] == 'i' else rng.uniform
|
|
76
|
+
vals = func(low=low, high=high, size=size)
|
|
77
|
+
return vals
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def gen_missing_vals(
|
|
81
|
+
vals: np.ndarray,
|
|
82
|
+
rng: np.random._generator.Generator,
|
|
83
|
+
dtype: str,
|
|
84
|
+
missing_pct: float = None,
|
|
85
|
+
) -> np.ndarray:
|
|
86
|
+
if missing_pct is None or missing_pct <= 0 or missing_pct >= 1:
|
|
87
|
+
return vals
|
|
88
|
+
if dtype == 's':
|
|
89
|
+
missing_val = None
|
|
90
|
+
elif dtype == 't':
|
|
91
|
+
missing_val = np.datetime64('NaT')
|
|
92
|
+
else:
|
|
93
|
+
missing_val = np.nan
|
|
94
|
+
if dtype == 'i':
|
|
95
|
+
vals = vals.astype(np.float64)
|
|
96
|
+
mask = rng.uniform(size=len(vals)) <= missing_pct
|
|
97
|
+
vals[mask] = missing_val
|
|
98
|
+
return vals
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sanitize_parameters(
|
|
102
|
+
name_prefix: str,
|
|
103
|
+
params: dict,
|
|
104
|
+
par_names: list[str],
|
|
105
|
+
):
|
|
106
|
+
if isinstance(params, int):
|
|
107
|
+
cnt = params
|
|
108
|
+
par = {}
|
|
109
|
+
else:
|
|
110
|
+
cnt = params['count']
|
|
111
|
+
par = {
|
|
112
|
+
k: (
|
|
113
|
+
list(v) + [None] * (cnt - len(v))
|
|
114
|
+
if isinstance(v, Iterable) and not isinstance(v, str)
|
|
115
|
+
else [v] * cnt
|
|
116
|
+
)
|
|
117
|
+
for k, v in params.items()
|
|
118
|
+
}
|
|
119
|
+
if name_prefix in ('i', 'f'):
|
|
120
|
+
par_names += ['dtype']
|
|
121
|
+
par['dtype'] = [name_prefix] * cnt
|
|
122
|
+
default_val = [None] * cnt
|
|
123
|
+
parameters = [
|
|
124
|
+
{key: par.get(key, default_val)[i] for key in par_names} for i in range(cnt)
|
|
125
|
+
]
|
|
126
|
+
col_names = par.get('name')
|
|
127
|
+
if col_names is None or col_names.count(None) > 1 or col_names.count('') > 1:
|
|
128
|
+
col_names = [f'{name_prefix}{i}' for i in range(1, cnt + 1)]
|
|
129
|
+
col_missing_pcts = par.get('missing_pct', default_val)
|
|
130
|
+
return col_names, parameters, col_missing_pcts
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def gen_rand_df(
|
|
134
|
+
nrow: int,
|
|
135
|
+
str_cols: dict = None,
|
|
136
|
+
ts_cols: dict = None,
|
|
137
|
+
int_cols: dict = None,
|
|
138
|
+
float_cols: dict = None,
|
|
139
|
+
rand_seed: int = 11,
|
|
140
|
+
) -> pd.DataFrame:
|
|
141
|
+
"""
|
|
142
|
+
Generate a random DataFrame with specified column types and parameters.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
nrow : int
|
|
147
|
+
Number of rows in the generated DataFrame.
|
|
148
|
+
str_cols : dict or int, optional
|
|
149
|
+
Parameters for string columns. If an integer is provided, it specifies the
|
|
150
|
+
number of string columns to generate with default parameters. If a dictionary
|
|
151
|
+
is provided, it should include the following keys:
|
|
152
|
+
|
|
153
|
+
- count: number of columns
|
|
154
|
+
- name: list of column names
|
|
155
|
+
- str_len: int or tuple of (min, max) string length
|
|
156
|
+
- str_chars: list of characters to use in strings
|
|
157
|
+
- col_strs: list of lists of strings to sample from for each column (if
|
|
158
|
+
provided, overrides str_len and str_chars)
|
|
159
|
+
ts_cols : dict or int, optional
|
|
160
|
+
Parameters for timestamp columns. If an integer is provided, it specifies the
|
|
161
|
+
number of timestamp columns to generate with default parameters. If a dictionary
|
|
162
|
+
is provided, it should include the following keys:
|
|
163
|
+
|
|
164
|
+
- count: number of columns
|
|
165
|
+
- name: list of column names
|
|
166
|
+
- start_date: start date for timestamp generation
|
|
167
|
+
- end_date: end date for timestamp generation
|
|
168
|
+
- freq: frequency for timestamp generation (e.g. 'D' for daily)
|
|
169
|
+
- random: whether to sample timestamps randomly from the generated range
|
|
170
|
+
int_cols : dict or int, optional
|
|
171
|
+
Parameters for integer columns. If an integer is provided, it specifies the
|
|
172
|
+
number of integer columns to generate with default parameters. If a dictionary
|
|
173
|
+
is provided, it should include the following keys:
|
|
174
|
+
|
|
175
|
+
- count: number of columns
|
|
176
|
+
- name: list of column names
|
|
177
|
+
- low: lower bound for random number generation
|
|
178
|
+
- high: upper bound for random number generation
|
|
179
|
+
- missing_pct: percentage of values to set as missing (NaN or None)
|
|
180
|
+
float_cols : dict or int, optional
|
|
181
|
+
Parameters for float columns. If an integer is provided, it specifies the
|
|
182
|
+
number of float columns to generate with default parameters. If a dictionary
|
|
183
|
+
is provided, it should include the following keys:
|
|
184
|
+
|
|
185
|
+
- count: number of columns
|
|
186
|
+
- name: list of column names
|
|
187
|
+
- low: lower bound for random number generation
|
|
188
|
+
- high: upper bound for random number generation
|
|
189
|
+
- missing_pct: percentage of values to set as missing (NaN or None)
|
|
190
|
+
rand_seed : int, optional
|
|
191
|
+
Random seed for reproducibility (default is 11).
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
pd.DataFrame
|
|
196
|
+
A DataFrame with the specified random data.
|
|
197
|
+
|
|
198
|
+
Examples
|
|
199
|
+
--------
|
|
200
|
+
|
|
201
|
+
Simply specify the number of columns for each type.
|
|
202
|
+
|
|
203
|
+
>>> import pandas as pd
|
|
204
|
+
>>> from mspu.data import gen_rand_df
|
|
205
|
+
>>> from mspu.pandas import pd_ht
|
|
206
|
+
>>> pd.DataFrame.ht = pd_ht
|
|
207
|
+
>>> df = gen_rand_df(
|
|
208
|
+
... nrow=365 * 24 * 60,
|
|
209
|
+
... str_cols=1,
|
|
210
|
+
... ts_cols=2,
|
|
211
|
+
... int_cols=1,
|
|
212
|
+
... float_cols=2,
|
|
213
|
+
... )
|
|
214
|
+
>>> df.ht()
|
|
215
|
+
shape: (525600, 6)
|
|
216
|
+
s1 t1 t2 i1 f1 f2
|
|
217
|
+
0 IZD8v 2024-01-01 2024-01-01 0 1.593760 1.648840
|
|
218
|
+
1 P9r1i 2024-01-02 2024-01-02 0 1.622772 1.943180
|
|
219
|
+
525598 iU68M NaT NaT 1 1.277124 0.093818
|
|
220
|
+
525599 P9r1i NaT NaT 1 1.248407 0.601518
|
|
221
|
+
|
|
222
|
+
Provide detailed parameters for each column type.
|
|
223
|
+
|
|
224
|
+
>>> import pandas as pd
|
|
225
|
+
>>> from mspu.data import gen_rand_df
|
|
226
|
+
>>> from mspu.pandas import pd_ht
|
|
227
|
+
>>> pd.DataFrame.ht = pd_ht
|
|
228
|
+
>>> d2 = gen_rand_df(
|
|
229
|
+
... nrow=10,
|
|
230
|
+
... str_cols={
|
|
231
|
+
... 'count': 2,
|
|
232
|
+
... 'name': ['country', 'color'],
|
|
233
|
+
... 'str_len': [3, (3, 9)],
|
|
234
|
+
... 'str_cnt': [2, 5],
|
|
235
|
+
... 'col_strs': [['UK', 'US', 'AU'], ['blue', 'black', 'red']],
|
|
236
|
+
... },
|
|
237
|
+
... ts_cols={
|
|
238
|
+
... 'count': 2,
|
|
239
|
+
... 'name': ['start_date', 'end_date'],
|
|
240
|
+
... 'start_date': ['2020-01-01', '2024-01-01'],
|
|
241
|
+
... 'end_date': ['2021-01-01', '2025-01-01'],
|
|
242
|
+
... 'freq': 'QS',
|
|
243
|
+
... 'random': False,
|
|
244
|
+
... },
|
|
245
|
+
... int_cols={
|
|
246
|
+
... 'count': 1,
|
|
247
|
+
... 'name': ['quantity'],
|
|
248
|
+
... 'low': [0],
|
|
249
|
+
... 'high': [100],
|
|
250
|
+
... 'missing_pct': [0.3],
|
|
251
|
+
... },
|
|
252
|
+
... float_cols={
|
|
253
|
+
... 'count': 2,
|
|
254
|
+
... 'name': ['price', 'charge'],
|
|
255
|
+
... 'low': [1, 0.1],
|
|
256
|
+
... 'high': [100, 0.9],
|
|
257
|
+
... 'missing_pct': [0.3, 0.2],
|
|
258
|
+
... },
|
|
259
|
+
... )
|
|
260
|
+
>>> d2.ht(1)
|
|
261
|
+
shape: (10, 7)
|
|
262
|
+
country color start_date end_date quantity price charge
|
|
263
|
+
0 UK black 2020-01-01 2024-01-01 86.0 NaN 0.657889
|
|
264
|
+
9 UK black NaT NaT 13.0 43.564921 0.776712
|
|
265
|
+
"""
|
|
266
|
+
col_types = ['s', 't', 'i', 'f']
|
|
267
|
+
col_params = [str_cols, ts_cols, int_cols, float_cols]
|
|
268
|
+
funcs = [gen_str_vals, gen_ts_vals, gen_num_vals, gen_num_vals]
|
|
269
|
+
par_names = [
|
|
270
|
+
['str_cnt', 'str_len', 'str_chars', 'col_strs'],
|
|
271
|
+
['start_date', 'end_date', 'freq', 'random'],
|
|
272
|
+
['low', 'high'],
|
|
273
|
+
['low', 'high'],
|
|
274
|
+
]
|
|
275
|
+
dfs = []
|
|
276
|
+
rand_rng = np.random.default_rng(seed=rand_seed)
|
|
277
|
+
for i, params in enumerate(col_params):
|
|
278
|
+
if params is None:
|
|
279
|
+
continue
|
|
280
|
+
col_names, col_params, col_missing_pcts = sanitize_parameters(
|
|
281
|
+
col_types[i], params, par_names[i]
|
|
282
|
+
)
|
|
283
|
+
df = pd.DataFrame(
|
|
284
|
+
{
|
|
285
|
+
col: gen_missing_vals(
|
|
286
|
+
funcs[i](nrow, rand_rng, **col_params[j]),
|
|
287
|
+
rand_rng,
|
|
288
|
+
col_types[i],
|
|
289
|
+
col_missing_pcts[j],
|
|
290
|
+
)
|
|
291
|
+
for j, col in enumerate(col_names)
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
dfs.append(df)
|
|
295
|
+
if len(dfs) == 0:
|
|
296
|
+
df = pd.DataFrame()
|
|
297
|
+
else:
|
|
298
|
+
df = pd.concat(dfs, axis=1)
|
|
299
|
+
return df
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == '__main__':
|
|
303
|
+
# example 1
|
|
304
|
+
df = gen_rand_df(
|
|
305
|
+
nrow=365 * 24 * 60,
|
|
306
|
+
str_cols=1,
|
|
307
|
+
ts_cols=2,
|
|
308
|
+
int_cols=1,
|
|
309
|
+
float_cols=2,
|
|
310
|
+
)
|
|
311
|
+
print(df[:2])
|
|
312
|
+
|
|
313
|
+
# example 2
|
|
314
|
+
d2 = gen_rand_df(
|
|
315
|
+
nrow=10,
|
|
316
|
+
str_cols={
|
|
317
|
+
'count': 2,
|
|
318
|
+
'name': ['country', 'color'],
|
|
319
|
+
'str_len': [3, (3, 9)],
|
|
320
|
+
'str_cnt': [2, 5],
|
|
321
|
+
'col_strs': [['UK', 'US', 'AU'], ['blue', 'black', 'red']],
|
|
322
|
+
},
|
|
323
|
+
ts_cols={
|
|
324
|
+
'count': 2,
|
|
325
|
+
'name': ['start_date', 'end_date'],
|
|
326
|
+
'start_date': ['2020-01-01', '2024-01-01'],
|
|
327
|
+
'end_date': ['2021-01-01', '2025-01-01'],
|
|
328
|
+
'freq': 'QS',
|
|
329
|
+
'random': False,
|
|
330
|
+
},
|
|
331
|
+
int_cols={
|
|
332
|
+
'count': 1,
|
|
333
|
+
'name': ['quantity'],
|
|
334
|
+
'low': [0],
|
|
335
|
+
'high': [100],
|
|
336
|
+
'missing_pct': [0.3],
|
|
337
|
+
},
|
|
338
|
+
float_cols={
|
|
339
|
+
'count': 2,
|
|
340
|
+
'name': ['price', 'charge'],
|
|
341
|
+
'low': [1, 0.1],
|
|
342
|
+
'high': [100, 0.9],
|
|
343
|
+
'missing_pct': [0.3, 0.2],
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
print(d2[:3])
|
|
File without changes
|
mspu/datetime/utils.py
ADDED
|
File without changes
|
mspu/io/__init__.py
ADDED
|
File without changes
|
mspu/io/file.py
ADDED
|
File without changes
|
mspu/pandas/__init__.py
ADDED
mspu/pandas/datetime.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def explode_date_range(
|
|
6
|
+
df: pd.DataFrame,
|
|
7
|
+
start_date_col: str,
|
|
8
|
+
end_date_col: str,
|
|
9
|
+
date_col: str = 'ts',
|
|
10
|
+
freq: str = '30min',
|
|
11
|
+
start_date_offset: pd.DateOffset = None,
|
|
12
|
+
end_date_offset: pd.DateOffset = None,
|
|
13
|
+
start_date_roll: Literal['backward', 'forward'] | None = None,
|
|
14
|
+
end_date_roll: Literal['backward', 'forward'] | None = None,
|
|
15
|
+
min_date: str | pd.Timestamp = None,
|
|
16
|
+
max_date: str | pd.Timestamp = None,
|
|
17
|
+
inclusive: Literal['both', 'left', 'right', 'neither'] = 'both',
|
|
18
|
+
drop_index: bool = True,
|
|
19
|
+
drop_date_cols: bool = True,
|
|
20
|
+
) -> pd.DataFrame:
|
|
21
|
+
"""
|
|
22
|
+
Explode DataFrame start/end date columns to date column.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
df:
|
|
27
|
+
The input DataFrame with start/end date columns/index levels.
|
|
28
|
+
start_date_col:
|
|
29
|
+
The column name in the DataFrame for the start date.
|
|
30
|
+
end_date_col:
|
|
31
|
+
The column name in the DataFrame for the end date.
|
|
32
|
+
date_col:
|
|
33
|
+
The column name in the DataFrame for the new date.
|
|
34
|
+
freq:
|
|
35
|
+
The frequency of the new date column.
|
|
36
|
+
start_date_offset:
|
|
37
|
+
The date offset for the start date column.
|
|
38
|
+
end_date_offset:
|
|
39
|
+
The date offset for the end date column.
|
|
40
|
+
start_date_roll:
|
|
41
|
+
Roll the start_date to the start of the current/next period.
|
|
42
|
+
end_date_roll:
|
|
43
|
+
Roll the end date to the start of the current/next period.
|
|
44
|
+
min_date:
|
|
45
|
+
The min value of the start date after offset.
|
|
46
|
+
max_date:
|
|
47
|
+
The max value of the end date after offset.
|
|
48
|
+
inclusive:
|
|
49
|
+
Include boundaries; Whether to set each bound as closed or open.
|
|
50
|
+
drop_index:
|
|
51
|
+
This flag should be False when the input DataFrame has meaningful index.
|
|
52
|
+
drop_date_cols:
|
|
53
|
+
Whether to drop the start_date_col and end_date_col or not.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
pl.DataFrame
|
|
58
|
+
The DataFrame same as the input but with
|
|
59
|
+
start/end date columns replaced by the new date column
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
Simple usage.
|
|
64
|
+
|
|
65
|
+
>>> df = pd.DataFrame({
|
|
66
|
+
... 'start_date': ['2023-01-01', '2023-01-02'],
|
|
67
|
+
... 'end_date': ['2023-01-01 01:00', '2023-01-02 02:00'],
|
|
68
|
+
... })
|
|
69
|
+
>>> df_exploded = explode_date_range(df, 'start_date', 'end_date', freq='1h')
|
|
70
|
+
>>> print(df_exploded)
|
|
71
|
+
ts
|
|
72
|
+
0 2023-01-01 00:00:00
|
|
73
|
+
1 2023-01-01 01:00:00
|
|
74
|
+
2 2023-01-02 00:00:00
|
|
75
|
+
3 2023-01-02 01:00:00
|
|
76
|
+
4 2023-01-02 02:00:00
|
|
77
|
+
|
|
78
|
+
With offset for start and end date.
|
|
79
|
+
|
|
80
|
+
>>> df = pd.DataFrame({
|
|
81
|
+
... 'start_date': ['2023-01-01', '2023-01-02'],
|
|
82
|
+
... 'end_date': ['2023-01-01 01:00:00', '2023-01-02 02:00:00'],
|
|
83
|
+
... })
|
|
84
|
+
>>> df_exploded = explode_date_range(
|
|
85
|
+
... df=df,
|
|
86
|
+
... start_date_col='start_date',
|
|
87
|
+
... end_date_col='end_date',
|
|
88
|
+
... freq='1h',
|
|
89
|
+
... start_date_offset=pd.DateOffset(hours=1),
|
|
90
|
+
... end_date_offset=pd.DateOffset(hours=-1),
|
|
91
|
+
... )
|
|
92
|
+
>>> print(df_exploded)
|
|
93
|
+
ts
|
|
94
|
+
0 2023-01-02 01:00:00
|
|
95
|
+
|
|
96
|
+
With rolling start and end date to the nearest frequency (hour).
|
|
97
|
+
|
|
98
|
+
>>> df = pd.DataFrame({
|
|
99
|
+
... 'start_date': ['2023-01-01 00:30', '2023-01-02 00:30'],
|
|
100
|
+
... 'end_date': ['2023-01-01 01:30', '2023-01-02 02:30'],
|
|
101
|
+
... })
|
|
102
|
+
>>> df_exploded = explode_date_range(
|
|
103
|
+
... df=df,
|
|
104
|
+
... start_date_col='start_date',
|
|
105
|
+
... end_date_col='end_date',
|
|
106
|
+
... freq='1h',
|
|
107
|
+
... start_date_roll='forward',
|
|
108
|
+
... end_date_roll='backward',
|
|
109
|
+
... )
|
|
110
|
+
>>> print(df_exploded)
|
|
111
|
+
ts
|
|
112
|
+
0 2023-01-01 01:00:00
|
|
113
|
+
1 2023-01-02 01:00:00
|
|
114
|
+
2 2023-01-02 02:00:00
|
|
115
|
+
"""
|
|
116
|
+
if not drop_index:
|
|
117
|
+
levels_old = list(df.index.names)
|
|
118
|
+
index_names = [
|
|
119
|
+
f'_idx{i}' if name is None else name for i, name in enumerate(levels_old)
|
|
120
|
+
]
|
|
121
|
+
df = df.rename_axis(index_names, axis=0)
|
|
122
|
+
if start_date_col in df.columns and end_date_col in df.columns:
|
|
123
|
+
levels = index_names
|
|
124
|
+
else:
|
|
125
|
+
if drop_date_cols:
|
|
126
|
+
levels = [
|
|
127
|
+
level
|
|
128
|
+
for level in index_names
|
|
129
|
+
if level not in (start_date_col, end_date_col)
|
|
130
|
+
]
|
|
131
|
+
levels_old = [
|
|
132
|
+
level
|
|
133
|
+
for level in levels_old
|
|
134
|
+
if level not in (start_date_col, end_date_col)
|
|
135
|
+
]
|
|
136
|
+
else:
|
|
137
|
+
levels = index_names
|
|
138
|
+
# move start/end_date_col to index if one exists
|
|
139
|
+
if start_date_col in df.columns:
|
|
140
|
+
levels += [start_date_col]
|
|
141
|
+
levels_old += [start_date_col]
|
|
142
|
+
elif end_date_col in df.columns:
|
|
143
|
+
levels += [end_date_col]
|
|
144
|
+
levels_old += [end_date_col]
|
|
145
|
+
levels += [date_col]
|
|
146
|
+
levels_old += [date_col]
|
|
147
|
+
|
|
148
|
+
df = df.reset_index(drop=drop_index).astype(
|
|
149
|
+
{
|
|
150
|
+
start_date_col: 'datetime64[ns]',
|
|
151
|
+
end_date_col: 'datetime64[ns]',
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# offset start date
|
|
156
|
+
if start_date_offset is not None:
|
|
157
|
+
df[start_date_col] += start_date_offset
|
|
158
|
+
|
|
159
|
+
# roll start date
|
|
160
|
+
if start_date_roll is not None:
|
|
161
|
+
roll_freq = freq if freq[-1] != 'S' else freq[:-1]
|
|
162
|
+
extra_period = 0 if start_date_roll == 'backward' else 1
|
|
163
|
+
df[start_date_col] = (
|
|
164
|
+
df[start_date_col].dt.to_period(roll_freq) + extra_period
|
|
165
|
+
).dt.start_time
|
|
166
|
+
|
|
167
|
+
# limit start_date and replace null with min_date
|
|
168
|
+
if min_date is not None:
|
|
169
|
+
min_date = pd.to_datetime(min_date, dayfirst=True)
|
|
170
|
+
df[start_date_col] = (
|
|
171
|
+
df[start_date_col]
|
|
172
|
+
.fillna(min_date)
|
|
173
|
+
.where(df[start_date_col] > min_date, min_date)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# offset end date
|
|
177
|
+
if end_date_offset is not None:
|
|
178
|
+
df[end_date_col] += end_date_offset
|
|
179
|
+
|
|
180
|
+
# roll end date
|
|
181
|
+
if end_date_roll is not None:
|
|
182
|
+
roll_freq = freq if freq[-1] != 'S' else freq[:-1]
|
|
183
|
+
extra_period = 0 if end_date_roll == 'backward' else 1
|
|
184
|
+
df[end_date_col] = (
|
|
185
|
+
df[end_date_col].dt.to_period(roll_freq) + extra_period
|
|
186
|
+
).dt.start_time
|
|
187
|
+
|
|
188
|
+
# limit end_date and replace null with max_date
|
|
189
|
+
if max_date is not None:
|
|
190
|
+
max_date = pd.to_datetime(max_date, dayfirst=True)
|
|
191
|
+
df[end_date_col] = (
|
|
192
|
+
df[end_date_col]
|
|
193
|
+
.fillna(max_date)
|
|
194
|
+
.where(df[end_date_col] < max_date, max_date)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# FIXME: special regarding pandas version
|
|
198
|
+
pd_version = tuple(int(n) for n in pd.__version__.split('.')[:3])
|
|
199
|
+
if pd_version >= (1, 4, 0):
|
|
200
|
+
inclusive_par = {'inclusive': inclusive}
|
|
201
|
+
else:
|
|
202
|
+
if inclusive == 'neither':
|
|
203
|
+
inclusive = 'right'
|
|
204
|
+
df[end_date_col] -= pd.DateOffset(microseconds=1)
|
|
205
|
+
inclusive_par = {'closed': None if inclusive == 'both' else inclusive}
|
|
206
|
+
|
|
207
|
+
# if inclusive = 'left' we expect date_end is exclusive
|
|
208
|
+
# so records with start_date == end_date should be excluded
|
|
209
|
+
# also reset index to ensure index start from 0 and is consecutive
|
|
210
|
+
if inclusive == 'left':
|
|
211
|
+
df = df.query(f'{start_date_col} < {end_date_col}').reset_index(drop=True)
|
|
212
|
+
else:
|
|
213
|
+
df = df.query(f'{start_date_col} <= {end_date_col}').reset_index(drop=True)
|
|
214
|
+
|
|
215
|
+
# get exploded timestamp column
|
|
216
|
+
if df.empty:
|
|
217
|
+
dt = (
|
|
218
|
+
pd.DataFrame(
|
|
219
|
+
{
|
|
220
|
+
'i': pd.Series(dtype='int'),
|
|
221
|
+
date_col: pd.Series(dtype='datetime64[ns]'),
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
.set_index('i')
|
|
225
|
+
.rename_axis(None, axis=0)
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
dt = (
|
|
229
|
+
pd.concat(
|
|
230
|
+
[
|
|
231
|
+
pd.DataFrame(
|
|
232
|
+
{
|
|
233
|
+
'i': i,
|
|
234
|
+
date_col: pd.date_range(
|
|
235
|
+
start=s, end=e, freq=freq, **inclusive_par
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
for i, (s, e) in enumerate(
|
|
240
|
+
zip(df[start_date_col], df[end_date_col])
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
.set_index('i')
|
|
245
|
+
.rename_axis(None, axis=0)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# drop start_date_col and end_date_col
|
|
249
|
+
if drop_date_cols:
|
|
250
|
+
df = df.drop(columns=[start_date_col, end_date_col])
|
|
251
|
+
|
|
252
|
+
# sample df based on new timestamp column
|
|
253
|
+
df = df.reindex(dt.index)
|
|
254
|
+
df[date_col] = dt[date_col]
|
|
255
|
+
|
|
256
|
+
# set index
|
|
257
|
+
if drop_index:
|
|
258
|
+
df = df.reset_index(drop=True)
|
|
259
|
+
else:
|
|
260
|
+
df = df.set_index(levels).rename_axis(levels_old, axis=0)
|
|
261
|
+
|
|
262
|
+
return df
|
mspu/pandas/parquet.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pa_mod(
|
|
5
|
+
ds: int | pd.Series,
|
|
6
|
+
divisor: int,
|
|
7
|
+
) -> int | pd.Series:
|
|
8
|
+
"""
|
|
9
|
+
Calculates remainder after division by a positive divisor.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
-----------
|
|
13
|
+
ds: int | pd.Series
|
|
14
|
+
An integer value or a pd.Series with int64[pyarrow] dtype
|
|
15
|
+
divisor: int
|
|
16
|
+
The positive divisor for the modulo operation
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
-----------
|
|
20
|
+
int | pd.Series:
|
|
21
|
+
An integer or pd.series containing the modulo results
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if divisor <= 0:
|
|
25
|
+
raise ValueError('Divisor must be a positive integer')
|
|
26
|
+
|
|
27
|
+
# Check if divisor is a power of 2
|
|
28
|
+
if divisor & (divisor - 1) == 0:
|
|
29
|
+
# Efficient bitwise AND for power-of-2 divisors
|
|
30
|
+
remainder = ds & (divisor - 1)
|
|
31
|
+
else:
|
|
32
|
+
# Slower integer division for non-power-of-2 divisors
|
|
33
|
+
quotient = ds // divisor
|
|
34
|
+
remainder = ds - (quotient * divisor)
|
|
35
|
+
|
|
36
|
+
return remainder
|
mspu/pandas/utils.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pd_ht(self, n: int = 2, c: int = None, w: int = None, r: int = None) -> None:
|
|
5
|
+
"""
|
|
6
|
+
Pandas head and tail in one command, with optional rounding.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
n : int
|
|
11
|
+
Number of rows to show from the head and tail. If n < 0 or n is greater than
|
|
12
|
+
half the number of rows, the entire DataFrame will be shown.
|
|
13
|
+
c : int
|
|
14
|
+
Number of columns to show. If None or greater than the number of columns,
|
|
15
|
+
all columns will be shown.
|
|
16
|
+
w : int
|
|
17
|
+
Display width in characters. If None, pandas default is used.
|
|
18
|
+
r : int
|
|
19
|
+
Number of decimal places to round float columns. If None or negative,
|
|
20
|
+
no rounding will be applied.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
None
|
|
25
|
+
|
|
26
|
+
Examples
|
|
27
|
+
--------
|
|
28
|
+
>>> import pandas as pd
|
|
29
|
+
>>> from mspu.pandas.utils import pd_ht
|
|
30
|
+
>>> pd.DataFrame.ht = pd_ht
|
|
31
|
+
>>> df = pd.DataFrame({
|
|
32
|
+
... 'foo': [1.12345, 2.98765, 3.14159],
|
|
33
|
+
... 'bar': [7, 8, 9],
|
|
34
|
+
... 'ham': ['x', 'y', 'z'],
|
|
35
|
+
... })
|
|
36
|
+
>>> df.ht(n=1, c=2, r=2)
|
|
37
|
+
shape: (3, 3)
|
|
38
|
+
foo ... ham
|
|
39
|
+
0 1.12 ... x
|
|
40
|
+
2 3.14 ... z
|
|
41
|
+
"""
|
|
42
|
+
# Columns to show
|
|
43
|
+
if isinstance(c, int) and c <= 0:
|
|
44
|
+
c = None
|
|
45
|
+
|
|
46
|
+
# Display width
|
|
47
|
+
if isinstance(w, int) and w <= 0:
|
|
48
|
+
w = 1000
|
|
49
|
+
|
|
50
|
+
# Slice head + tail (or full df)
|
|
51
|
+
if n < 0 or self.shape[0] <= 2 * n:
|
|
52
|
+
df = self
|
|
53
|
+
else:
|
|
54
|
+
df = pd.concat([self.iloc[:n], self.iloc[-n:]])
|
|
55
|
+
|
|
56
|
+
# Round float columns
|
|
57
|
+
if r is not None and r >= 0:
|
|
58
|
+
float_cols = df.select_dtypes(include='float').columns
|
|
59
|
+
df[float_cols] = df[float_cols].round(r)
|
|
60
|
+
|
|
61
|
+
with pd.option_context(
|
|
62
|
+
'display.show_dimensions',
|
|
63
|
+
False,
|
|
64
|
+
'display.width',
|
|
65
|
+
w,
|
|
66
|
+
'display.max_rows',
|
|
67
|
+
df.shape[0],
|
|
68
|
+
'display.max_columns',
|
|
69
|
+
c,
|
|
70
|
+
):
|
|
71
|
+
print(f'shape: {self.shape}')
|
|
72
|
+
print(df)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def remove_cols_utc(df: pd.DataFrame) -> pd.DataFrame:
|
|
76
|
+
"""
|
|
77
|
+
Converts datetime columns in UTC to timezone-naive (tz=None).
|
|
78
|
+
"""
|
|
79
|
+
for col in df.select_dtypes(include=['datetimetz']).columns:
|
|
80
|
+
tz = df[col].dt.tz
|
|
81
|
+
if tz is not None and 'utc' in str(tz).lower():
|
|
82
|
+
df[col] = df[col].dt.tz_localize(None)
|
|
83
|
+
return df
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def df_diffs(
|
|
87
|
+
df1: pd.DataFrame,
|
|
88
|
+
df2: pd.DataFrame,
|
|
89
|
+
left_suffix: str = '_df1',
|
|
90
|
+
right_suffix: str = '_df2',
|
|
91
|
+
) -> pd.DataFrame:
|
|
92
|
+
"""
|
|
93
|
+
Get rows in two dfs that are different to each other.
|
|
94
|
+
|
|
95
|
+
Comparison is based on df MultiIndex.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
df1 : pd.DataFrame
|
|
100
|
+
First DataFrame to compare.
|
|
101
|
+
df2 : pd.DataFrame
|
|
102
|
+
Second DataFrame to compare.
|
|
103
|
+
left_suffix : str
|
|
104
|
+
Suffix to append to column names from df1 in the output DataFrame.
|
|
105
|
+
right_suffix : str
|
|
106
|
+
Suffix to append to column names from df2 in the output DataFrame.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
pd.DataFrame
|
|
111
|
+
DataFrame containing rows that are different between df1 and df2, with suffixes
|
|
112
|
+
indicating the source of each column.
|
|
113
|
+
|
|
114
|
+
Examples
|
|
115
|
+
--------
|
|
116
|
+
>>> import pandas as pd
|
|
117
|
+
>>> from mspu.pandas import df_diffs
|
|
118
|
+
>>> df1 = pd.DataFrame({
|
|
119
|
+
... 'fruit': ['apple', 'banana', 'apple'],
|
|
120
|
+
... 'id': [1, 2, 4],
|
|
121
|
+
... 'store': ['us', 'uk', 'cn'],
|
|
122
|
+
... 'price': [4, 3.1, 2.5],
|
|
123
|
+
... }).set_index(['fruit', 'id'])
|
|
124
|
+
>>> print(df1)
|
|
125
|
+
store price
|
|
126
|
+
fruit id
|
|
127
|
+
apple 1 us 4.0
|
|
128
|
+
banana 2 uk 3.1
|
|
129
|
+
apple 4 cn 2.5
|
|
130
|
+
>>> df2 = pd.DataFrame({
|
|
131
|
+
... 'fruit': ['apple', 'banana', 'apple'],
|
|
132
|
+
... 'id': [1, 3, 4],
|
|
133
|
+
... 'store': ['us', 'uk', 'cn'],
|
|
134
|
+
... 'price': [4, 3.2, 2.5],
|
|
135
|
+
... }).set_index(['fruit', 'id'])
|
|
136
|
+
>>> print(df2)
|
|
137
|
+
store price
|
|
138
|
+
fruit id
|
|
139
|
+
apple 1 us 4.0
|
|
140
|
+
banana 3 uk 3.2
|
|
141
|
+
apple 4 cn 2.5
|
|
142
|
+
>>> diff = df_diffs(df1, df2)
|
|
143
|
+
>>> print(diff)
|
|
144
|
+
store_df1 price_df1 store_df2 price_df2
|
|
145
|
+
fruit id
|
|
146
|
+
banana 2 uk 3.1 NaN NaN
|
|
147
|
+
3 NaN NaN uk 3.2
|
|
148
|
+
"""
|
|
149
|
+
# Perform an outer join
|
|
150
|
+
df_joined = pd.merge(
|
|
151
|
+
df1,
|
|
152
|
+
df2,
|
|
153
|
+
how='outer',
|
|
154
|
+
left_index=True,
|
|
155
|
+
right_index=True,
|
|
156
|
+
suffixes=(left_suffix, right_suffix),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Compare the columns to find differences
|
|
160
|
+
d1_joined = df_joined.filter(like=left_suffix).rename(
|
|
161
|
+
columns=lambda x: x.rsplit('_', 1)[0]
|
|
162
|
+
)
|
|
163
|
+
d2_joined = df_joined.filter(like=right_suffix).rename(
|
|
164
|
+
columns=lambda x: x.rsplit('_', 1)[0]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Compare two dfs with `ne`, col names must be the same
|
|
168
|
+
diffs = df_joined[d1_joined.ne(d2_joined).any(axis=1)]
|
|
169
|
+
|
|
170
|
+
return diffs
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == '__main__':
|
|
174
|
+
# Create two example DataFrames with string columns
|
|
175
|
+
df1 = pd.DataFrame(
|
|
176
|
+
{
|
|
177
|
+
'fruit': ['apple', 'banana', 'apple'],
|
|
178
|
+
'id': [1, 2, 4],
|
|
179
|
+
'store': ['us', 'uk', 'cn'],
|
|
180
|
+
'price': [4, 3.1, 2.5],
|
|
181
|
+
}
|
|
182
|
+
).set_index(['fruit', 'id'])
|
|
183
|
+
|
|
184
|
+
df2 = pd.DataFrame(
|
|
185
|
+
{
|
|
186
|
+
'fruit': ['apple', 'banana', 'apple'],
|
|
187
|
+
'id': [1, 3, 4],
|
|
188
|
+
'store': ['us', 'uk', 'cn'],
|
|
189
|
+
'price': [4, 3.2, 2.5],
|
|
190
|
+
}
|
|
191
|
+
).set_index(['fruit', 'id'])
|
|
192
|
+
|
|
193
|
+
df = df_diffs(df1, df2)
|
|
194
|
+
|
|
195
|
+
# Show the resulting differences
|
|
196
|
+
print(df)
|
mspu/polars/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .utils import (
|
|
2
|
+
pl_ht,
|
|
3
|
+
inf_count,
|
|
4
|
+
nan_count,
|
|
5
|
+
nul_count,
|
|
6
|
+
lowercase_polars_df,
|
|
7
|
+
to_float32_polars_df,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'pl_ht',
|
|
12
|
+
'inf_count',
|
|
13
|
+
'nan_count',
|
|
14
|
+
'nul_count',
|
|
15
|
+
'lowercase_polars_df',
|
|
16
|
+
'to_float32_polars_df',
|
|
17
|
+
]
|
mspu/polars/utils.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
import polars.selectors as cs
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def pl_ht(self, n: int = 2, c: int = None, w: int = None, r: int = None) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Polars head and tail in one command, with optional rounding.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
n : int
|
|
13
|
+
Number of rows to show from the head and tail. If n < 0 or n is greater than
|
|
14
|
+
half the number of rows, the entire DataFrame will be shown.
|
|
15
|
+
c : int
|
|
16
|
+
Number of columns to show. If None or greater than the number of columns,
|
|
17
|
+
all columns will be shown.
|
|
18
|
+
w : int
|
|
19
|
+
Width of the output in characters. If None, the width will be determined.
|
|
20
|
+
r : int
|
|
21
|
+
Number of decimal places to round float and decimal columns. If None or negative,
|
|
22
|
+
no rounding will be applied.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
None
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> import polars as pl
|
|
31
|
+
>>> from mspu.polars.utils import pl_ht
|
|
32
|
+
>>> pl.DataFrame.ht = pl_ht
|
|
33
|
+
>>> df = pl.DataFrame({
|
|
34
|
+
... 'foo': [1.12345, 2.98765, 3.14159],
|
|
35
|
+
... 'bar': [7, 8, 9],
|
|
36
|
+
... 'ham': ['x', 'y', 'z'],
|
|
37
|
+
... })
|
|
38
|
+
>>> df.ht(n=1, c=2, r=2)
|
|
39
|
+
shape: (3, 3)
|
|
40
|
+
┌──────┬───┬─────┐
|
|
41
|
+
│ foo ┆ … ┆ ham │
|
|
42
|
+
│ --- ┆ ┆ --- │
|
|
43
|
+
│ f64 ┆ ┆ str │
|
|
44
|
+
╞══════╪═══╪═════╡
|
|
45
|
+
│ 1.12 ┆ … ┆ x │
|
|
46
|
+
│ 3.14 ┆ … ┆ z │
|
|
47
|
+
└──────┴───┴─────┘
|
|
48
|
+
"""
|
|
49
|
+
if n < 0 or self.shape[0] < 2 * n:
|
|
50
|
+
df = self
|
|
51
|
+
else:
|
|
52
|
+
df = pl.concat([self[:n], self[-n:]])
|
|
53
|
+
if r is not None and r >= 0:
|
|
54
|
+
df = df.with_columns((cs.float() | cs.decimal()).round(r))
|
|
55
|
+
with pl.Config(
|
|
56
|
+
tbl_hide_dataframe_shape=True,
|
|
57
|
+
tbl_width_chars=w,
|
|
58
|
+
tbl_rows=df.shape[0],
|
|
59
|
+
tbl_cols=c,
|
|
60
|
+
):
|
|
61
|
+
print(f'shape: {self.shape}')
|
|
62
|
+
print(df)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parquet_to_csv(filepath: str):
|
|
66
|
+
"""
|
|
67
|
+
Read parquet and save as csv.
|
|
68
|
+
"""
|
|
69
|
+
filepath_csv = f'{filepath[:-7]}csv'
|
|
70
|
+
pl.read_parquet(filepath).write_csv(filepath_csv)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def lowercase_polars_df(
|
|
74
|
+
df: pl.DataFrame,
|
|
75
|
+
lowercase: Literal['header', 'columns', 'both'] = 'both',
|
|
76
|
+
) -> pl.DataFrame:
|
|
77
|
+
"""
|
|
78
|
+
Converts all column names and string columns to lowercase.
|
|
79
|
+
"""
|
|
80
|
+
# Lowercase column headers
|
|
81
|
+
if lowercase in ('header', 'both'):
|
|
82
|
+
df = df.rename({col: col.lower() for col in df.columns})
|
|
83
|
+
# Lowercase string columns
|
|
84
|
+
if lowercase in ('columns', 'both'):
|
|
85
|
+
df = df.with_columns([cs.string().str.to_lowercase()])
|
|
86
|
+
return df
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def to_float32_polars_df(df: pl.DataFrame) -> pl.DataFrame:
|
|
90
|
+
"""
|
|
91
|
+
Convert all numerical columns type to float32.
|
|
92
|
+
"""
|
|
93
|
+
df = df.with_columns((cs.float() | cs.decimal()).cast(pl.Float32))
|
|
94
|
+
return df
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def inf_count(df: pl.DataFrame) -> pl.DataFrame:
|
|
98
|
+
"""
|
|
99
|
+
Counts the number of infinite values in each column of a Polars DataFrame.
|
|
100
|
+
|
|
101
|
+
Returns a new DataFrame with the column names and their corresponding counts,
|
|
102
|
+
sorted in descending order.
|
|
103
|
+
"""
|
|
104
|
+
df_inf = (
|
|
105
|
+
df.select([(pl.col(col).is_infinite().sum().alias(col)) for col in df.columns])
|
|
106
|
+
.unpivot(variable_name='col', value_name='inf_cnt')
|
|
107
|
+
.filter(pl.col('inf_cnt') > 0)
|
|
108
|
+
.sort('inf_cnt', descending=True)
|
|
109
|
+
)
|
|
110
|
+
return df_inf
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def nan_count(df: pl.DataFrame) -> pl.DataFrame:
|
|
114
|
+
"""
|
|
115
|
+
Counts the number of NaN values in each column of a Polars DataFrame.
|
|
116
|
+
|
|
117
|
+
Returns a new DataFrame with the column names and their corresponding counts,
|
|
118
|
+
sorted in descending order.
|
|
119
|
+
"""
|
|
120
|
+
df_nan = (
|
|
121
|
+
df.select([(pl.col(col).is_nan().sum().alias(col)) for col in df.columns])
|
|
122
|
+
.unpivot(variable_name='col', value_name='nan_cnt')
|
|
123
|
+
.filter(pl.col('nan_cnt') > 0)
|
|
124
|
+
.sort('nan_cnt', descending=True)
|
|
125
|
+
)
|
|
126
|
+
return df_nan
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def nul_count(df: pl.DataFrame) -> pl.DataFrame:
|
|
130
|
+
"""
|
|
131
|
+
Counts the number of null values in each column of a Polars DataFrame.
|
|
132
|
+
|
|
133
|
+
Returns a new DataFrame with the column names and their corresponding counts,
|
|
134
|
+
sorted in descending order.
|
|
135
|
+
"""
|
|
136
|
+
df_nul = (
|
|
137
|
+
df.select([(pl.col(col).is_null().sum().alias(col)) for col in df.columns])
|
|
138
|
+
.unpivot(variable_name='col', value_name='nul_cnt')
|
|
139
|
+
.filter(pl.col('nul_cnt') > 0)
|
|
140
|
+
.sort('nul_cnt', descending=True)
|
|
141
|
+
)
|
|
142
|
+
return df_nul
|
mspu/security/crypt.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
from cryptography.fernet import Fernet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_key(keypath: str):
|
|
8
|
+
# Generate a key
|
|
9
|
+
key = Fernet.generate_key()
|
|
10
|
+
|
|
11
|
+
# Save the key securely
|
|
12
|
+
with open(keypath, 'wb') as f:
|
|
13
|
+
f.write(key)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_key(keypath: str) -> bytes:
|
|
17
|
+
# Load key from file
|
|
18
|
+
with open(keypath, 'rb') as f:
|
|
19
|
+
key = f.read()
|
|
20
|
+
return key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def encrypt_txt(key: bytes, txt: str) -> bytes:
|
|
24
|
+
# Serialize and encrypt the txt
|
|
25
|
+
cipher = Fernet(key)
|
|
26
|
+
encrypted_credentials = cipher.encrypt(txt)
|
|
27
|
+
return encrypted_credentials
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def decrypt_txt(key: bytes, txt: bytes) -> bytes:
|
|
31
|
+
# Decrypt the credentials
|
|
32
|
+
cipher = Fernet(key)
|
|
33
|
+
decrypted_credentials = cipher.decrypt(txt)
|
|
34
|
+
return decrypted_credentials
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def encrypt_file(key: bytes, filepath: str):
|
|
38
|
+
# Load the credentials
|
|
39
|
+
with open(filepath, 'rb') as f:
|
|
40
|
+
txt = f.read()
|
|
41
|
+
# Serialize and encrypt the txt
|
|
42
|
+
encrypted_credentials = encrypt_txt(key, txt)
|
|
43
|
+
# Save the encrypted file
|
|
44
|
+
file, ext = filepath.rsplit('.', 1)
|
|
45
|
+
filepath = f'{file}_.{ext}'
|
|
46
|
+
with open(filepath, 'wb') as f:
|
|
47
|
+
f.write(encrypted_credentials)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decrypt_file(key: bytes, filepath: str, to_json: bool = True):
|
|
51
|
+
# Load the credentials
|
|
52
|
+
with open(filepath, 'rb') as f:
|
|
53
|
+
txt = f.read()
|
|
54
|
+
# Decrypt the credentials
|
|
55
|
+
decrypted_credentials = decrypt_txt(key, txt).decode()
|
|
56
|
+
# Load the credentials into a dictionary
|
|
57
|
+
if to_json:
|
|
58
|
+
decrypted_credentials = json.loads(decrypted_credentials)
|
|
59
|
+
return decrypted_credentials
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ha64(txt: str):
|
|
63
|
+
txt = hashlib.sha256(txt.encode()).digest()
|
|
64
|
+
return base64.urlsafe_b64encode(txt)
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
fp = 'c:/files'
|
|
68
|
+
# create_key(f'{fp}/my-key.json')
|
|
69
|
+
|
|
70
|
+
key = load_key(f'{fp}/my-key.json')
|
|
71
|
+
|
|
72
|
+
key = hashlib.sha256('my-key'.encode()).digest()
|
|
73
|
+
key = base64.urlsafe_b64encode(key)
|
|
74
|
+
fl = 'my-key.json'
|
|
75
|
+
# encrypt_file(key, f'{fp}/{fl}')
|
|
76
|
+
|
|
77
|
+
key = decrypt_file(key, f'{fp}/my-key_.json', False)
|
|
78
|
+
db_cred = decrypt_file(key, f'{fp}/credentials_.json')
|
|
79
|
+
|
|
80
|
+
print(db_cred)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mspu
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Ma's python utils package.
|
|
5
|
+
Author-email: Sean Ma <sean.sl.ma@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Requires-Dist: polars
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# mspu
|
|
15
|
+
|
|
16
|
+
My python utils package. It includes some commonly used functions, like `gen_rand_df` and `explode_date_range`.
|
|
17
|
+
|
|
18
|
+
## How to install `mspu`
|
|
19
|
+
|
|
20
|
+
Using `pip`:
|
|
21
|
+
```sh
|
|
22
|
+
pip install mspu
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Using `uv`:
|
|
26
|
+
```sh
|
|
27
|
+
uv pip install mspu
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## The document for `mspu`
|
|
31
|
+
|
|
32
|
+
Visit the [document](https://seanslma.github.io/mspu) for more details.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mspu/__init__.py,sha256=uWOTnD7uFq1Qca0T90qOTjvIq5on-r64Ug4hJWPYQ64,184
|
|
2
|
+
mspu/data/__init__.py,sha256=THICntQKY0THohm-I1gPwi9I0yJHHg5Jv0IVCgZ8uAA,67
|
|
3
|
+
mspu/data/rand_df.py,sha256=I1Hkc8NfrLamff2fUG6il0KW67epoVrvq9TrKJSVS6w,10841
|
|
4
|
+
mspu/datetime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mspu/datetime/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
mspu/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
mspu/io/file.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
mspu/pandas/__init__.py,sha256=gommfn2jXrmUBnXG2qMtQzqRMg1oEqyLPMGmRzGSgmA,188
|
|
9
|
+
mspu/pandas/datetime.py,sha256=_iAZ9xqFJ-tYnT3x5hT09wt0Ihlc21PPaBOfFyL8boc,8656
|
|
10
|
+
mspu/pandas/parquet.py,sha256=XjjdpgNG7Qsm3KbpbuR1qfGKFnq1FdPaoJzmA19A7I8,915
|
|
11
|
+
mspu/pandas/utils.py,sha256=3JzKQDssYzxJosgtARc90EFQCb24yHqyNmGJlWx6dDk,5542
|
|
12
|
+
mspu/polars/__init__.py,sha256=dh4ekg-o_Gv4-nnnYI3flRliNn9BF16fJJE97_vCBzI,264
|
|
13
|
+
mspu/polars/utils.py,sha256=VMZrljTV7mW7Uy0SpMRS2aV6whhTIgDMYrRNbHf7bIA,4511
|
|
14
|
+
mspu/security/crypt.py,sha256=iYnkg9YdPJjEgYrw7iAnpK0qiH0illpp1-bIkZpIxF0,2097
|
|
15
|
+
mspu-0.0.1.dist-info/METADATA,sha256=ZHGLKpsG-e2WwuBTpCxzVpOT9hmBDHKFgRqJQC9BKd8,625
|
|
16
|
+
mspu-0.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
17
|
+
mspu-0.0.1.dist-info/licenses/LICENSE,sha256=Rc482qeBCHvWChKavd4nCgfkKshLEHDcmTlXNOIQ6DY,1064
|
|
18
|
+
mspu-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sean Ma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|