meerschaum.utils.dataframe
Utility functions for working with DataFrames.
1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Utility functions for working with DataFrames. 7""" 8 9from __future__ import annotations 10 11from datetime import datetime, date 12from collections import defaultdict 13 14import meerschaum as mrsm 15from meerschaum.utils.typing import ( 16 Optional, Dict, Any, List, Hashable, Generator, 17 Iterator, Iterable, Union, TYPE_CHECKING, Tuple, 18) 19from meerschaum import _core 20 21if TYPE_CHECKING: 22 pd, dask = mrsm.attempt_import('pandas', 'dask') 23 24 25#: The DataFrame libraries Meerschaum returns frames in (`meerschaum:dataframe`). 26DATAFRAME_LIBRARIES: Tuple[str, ...] = ('polars', 'pandas') 27 28#: The environment variable that overrides `meerschaum:dataframe` for one process (and the jobs 29#: and workers it starts). 30DATAFRAME_ENV_VAR: str = 'MRSM_DATAFRAME' 31 32#: The library of a root that does not say (a fresh root writes `dataframe: polars`). 33DEFAULT_DATAFRAME_LIBRARY: str = 'polars' 34 35#: The extra that installs each library. 36DATAFRAME_EXTRAS: Dict[str, str] = {'polars': 'polars', 'pandas': 'pandas'} 37 38 39def _normalize_dataframe_library(value: Any, source: str) -> str: 40 """Return `'polars'` or `'pandas'` for a configured value, or raise naming `source`.""" 41 if value is None: 42 return DEFAULT_DATAFRAME_LIBRARY 43 text = str(value).strip().lower() 44 if not text: 45 return DEFAULT_DATAFRAME_LIBRARY 46 if text not in DATAFRAME_LIBRARIES: 47 raise ValueError( 48 f"Unknown DataFrame library '{value}' ({source}): use 'polars' or 'pandas'." 49 ) 50 return text 51 52 53def get_dataframe_library() -> str: 54 """ 55 Return the DataFrame library Meerschaum returns frames in: `'polars'` or `'pandas'`. 56 57 The setting is `dataframe` in `meerschaum.yaml` (config path `meerschaum:dataframe`), and the 58 `MRSM_DATAFRAME` environment variable overrides it for one process (and the jobs it starts). 59 A fresh root writes `dataframe: polars`; a root created by Meerschaum 4 or by an earlier 5.0 60 release candidate is given `dataframe: pandas` the first time 5.0.0rc15 or newer opens it. 61 62 Every API that returns a DataFrame follows it (`Pipe.get_data()`, `SQLConnector.read()`, 63 `Pipe.fetch()`, `Plan.to_dataframe()`, the frames handed to plugin code, ...), and 64 `as_polars=True` / `as_pandas=True` override it for one call. 65 66 Returns 67 ------- 68 `'polars'` or `'pandas'`. 69 70 Examples 71 -------- 72 >>> import os 73 >>> os.environ['MRSM_DATAFRAME'] = 'pandas' 74 >>> get_dataframe_library() 75 'pandas' 76 """ 77 import os 78 value = os.environ.get(DATAFRAME_ENV_VAR, None) 79 if value is not None and value.strip(): 80 return _normalize_dataframe_library(value, DATAFRAME_ENV_VAR) 81 from meerschaum.config import get_config 82 return _normalize_dataframe_library( 83 get_config('meerschaum', 'dataframe', warn=False), 84 'meerschaum:dataframe', 85 ) 86 87 88def resolve_dataframe_library( 89 as_polars: Optional[bool] = None, 90 as_pandas: Optional[bool] = None, 91) -> str: 92 """ 93 Return the library one call returns its frames in: `'polars'` or `'pandas'`. 94 95 Parameters 96 ---------- 97 as_polars: Optional[bool], default None 98 `True` asks for Polars and `False` for pandas (Meerschaum 4's meaning of 99 `as_polars=False`). `None` follows `get_dataframe_library()`. 100 101 as_pandas: Optional[bool], default None 102 `True` asks for pandas and `False` for Polars. `None` follows the setting. 103 104 Returns 105 ------- 106 `'polars'` or `'pandas'`. Asking for both (or for neither) raises a `ValueError`. 107 """ 108 wants_polars = None if as_polars is None else bool(as_polars) 109 wants_pandas = None if as_pandas is None else bool(as_pandas) 110 if wants_polars is not None and wants_pandas is not None and wants_polars == wants_pandas: 111 raise ValueError( 112 "`as_polars` and `as_pandas` contradict each other: pass only one of them." 113 ) 114 if wants_polars is not None: 115 return 'polars' if wants_polars else 'pandas' 116 if wants_pandas is not None: 117 return 'pandas' if wants_pandas else 'polars' 118 return get_dataframe_library() 119 120 121def wants_polars( 122 as_polars: Optional[bool] = None, 123 as_pandas: Optional[bool] = None, 124) -> bool: 125 """Return whether one call returns Polars frames (see `resolve_dataframe_library()`).""" 126 return resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) == 'polars' 127 128 129def import_dataframe_library(library: Optional[str] = None) -> Any: 130 """ 131 Import the `polars` or `pandas` module (the configured one by default). 132 133 A missing library is installed into the `mrsm` virtual environment when automatic installs are 134 allowed (as for any other optional package); otherwise the `ImportError` names the extra to 135 install (`pip install meerschaum[polars]` or `pip install meerschaum[pandas]`). 136 """ 137 library = library or get_dataframe_library() 138 from meerschaum.utils.packages import attempt_import, import_pandas 139 module = None 140 try: 141 if library == 'pandas': 142 module = import_pandas(warn=False) 143 else: 144 module = attempt_import(library, lazy=False, warn=False) 145 except Exception: 146 module = None 147 if module is None: 148 raise missing_dataframe_library_error(library) 149 return module 150 151 152def ensure_dataframe_library(library: Optional[str] = None) -> Any: 153 """ 154 Return the `polars` or `pandas` module (the configured one by default), importing it on the 155 first call (`import_dataframe_library()`: installed on first use when automatic installs 156 are allowed, else an `ImportError` naming the extra). Cheap once the module is imported. 157 """ 158 import sys 159 library = library or get_dataframe_library() 160 module = sys.modules.get(library, None) 161 if module is not None and not getattr(getattr(module, '__spec__', None), '_initializing', False): 162 if library != 'pandas' or 'pyarrow' in sys.modules: 163 return module 164 return import_dataframe_library(library) 165 166 167def missing_dataframe_library_error(library: str) -> ImportError: 168 """The `ImportError` for a DataFrame library that is not installed (names the extra).""" 169 extra = DATAFRAME_EXTRAS.get(library, library) 170 other = 'pandas' if library == 'polars' else 'polars' 171 return ImportError( 172 f"Meerschaum returns {library} DataFrames here, but {library} is not installed. " 173 f"Install it with `pip install meerschaum[{extra}]`, " 174 f"or set `dataframe: {other}` in meerschaum.yaml (`MRSM_DATAFRAME={other}`)." 175 ) 176 177 178def is_polars_frame(df: Any) -> bool: 179 """Whether `df` is a Polars DataFrame or LazyFrame.""" 180 return (type(df).__module__ or '').split('.')[0] == 'polars' 181 182 183def is_pandas_frame(df: Any) -> bool: 184 """Whether `df` is a pandas (or GeoPandas) DataFrame.""" 185 return ( 186 (type(df).__module__ or '').split('.')[0] in ('pandas', 'geopandas') 187 and hasattr(df, 'iloc') 188 ) 189 190 191def frame_library_of(df: Any) -> Optional[str]: 192 """`'polars'`, `'pandas'` or `None`: the library of a DataFrame (or `None` for other data).""" 193 if is_polars_frame(df): 194 return 'polars' 195 if is_pandas_frame(df): 196 return 'pandas' 197 return None 198 199 200def output_library_for( 201 df: Any, 202 as_polars: Optional[bool] = None, 203 as_pandas: Optional[bool] = None, 204) -> str: 205 """ 206 The library a function that transforms a frame returns: what the call asks for 207 (`as_polars` / `as_pandas`), else the library `df` came in (a Polars frame stays Polars, a 208 pandas frame pandas), else (documents, dictionaries, Arrow data) `get_dataframe_library()`. 209 """ 210 if as_polars is not None or as_pandas is not None: 211 return resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 212 return frame_library_of(df) or get_dataframe_library() 213 214 215def frame_from_arrow( 216 data: Any, 217 *, 218 as_polars: Optional[bool] = None, 219 as_pandas: Optional[bool] = None, 220 dtypes: Optional[Dict[str, str]] = None, 221 debug: bool = False, 222) -> Any: 223 """ 224 Build the configured library's DataFrame from Arrow data (the one conversion every 225 frame-returning API uses). 226 227 Parameters 228 ---------- 229 data: Any 230 Anything that implements the Arrow PyCapsule interface (`__arrow_c_stream__` or 231 `__arrow_c_array__`): a `meerschaum._core.ArrowStream`, a `pyarrow.Table`, 232 `RecordBatch` or `RecordBatchReader`, ... or a list (or tuple) of record batches with one 233 schema. A pandas or Polars DataFrame is converted to the requested library. 234 `None` returns `None`. 235 236 as_polars: Optional[bool], default None 237 `True` returns Polars, `False` pandas; `None` follows `get_dataframe_library()`. 238 239 as_pandas: Optional[bool], default None 240 `True` returns pandas, `False` Polars; `None` follows the setting. 241 242 dtypes: Optional[Dict[str, str]], default None 243 Per-column dtypes: Meerschaum dtype strings (`'int'`, `'json'`, `'geometry[Point, 4326]'`, 244 ...) or pandas dtype strings (`'Int64'`, `'int64[pyarrow]'`, ...). 245 For pandas they restore the exact pandas dtype within the column's kind, as every read of 246 the façade does (`int64` vs `Int64` vs `int64[pyarrow]`, the datetime unit); a dtype of 247 another kind does not cast the column. For Polars, declared `json` and `geometry` 248 columns become extension columns even when every value is null. 249 250 debug: bool, default False 251 Verbosity toggle. 252 253 Returns 254 ------- 255 A `polars.DataFrame` or a `pandas.DataFrame` (a `geopandas.GeoDataFrame` when there are 256 geometry columns): JSON, UUID, numeric and geometry columns are restored from the Arrow 257 field metadata the kernel writes (`ARROW:extension:name`, `meerschaum:dtype`). Polars frames 258 carry geometry as GeoArrow WKB (binary) with the CRS in the field metadata. 259 260 Examples 261 -------- 262 >>> import pyarrow as pa 263 >>> frame_from_arrow(pa.table({'a': [1, None]}), as_pandas=True).dtypes 264 a Int64 265 dtype: object 266 >>> frame_from_arrow(pa.table({'a': [1, None]}), as_polars=True).schema 267 Schema([('a', Int64)]) 268 """ 269 if data is None: 270 return None 271 library = resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 272 to_polars_frame = library == 'polars' 273 current = frame_library_of(data) 274 if current is not None: 275 if current == library: 276 return data 277 return to_polars(data) if to_polars_frame else to_pandas(data) 278 ensure_dataframe_library(library) 279 if isinstance(data, (list, tuple)): 280 data = list(data) 281 _rust = _core.frame_from_arrow 282 return _rust( 283 data, 284 as_polars=to_polars_frame, 285 dtypes=({str(col): str(typ) for col, typ in dtypes.items()} if dtypes else None), 286 debug=debug, 287 ) 288 289 290def to_pandas(df: Any) -> Any: 291 """Convert a Polars DataFrame or LazyFrame to Pandas; otherwise return ``df``.""" 292 if df.__class__.__module__.split('.')[0] != 'polars': 293 return df 294 if df.__class__.__name__ == 'LazyFrame': 295 df = df.collect() 296 json_cols = [ 297 col 298 for col, typ in df.schema.items() 299 if getattr(typ, 'ext_name', lambda: None)() == 'arrow.json' 300 ] 301 if json_cols: 302 df = df.with_columns(df[col].ext.storage() for col in json_cols) 303 pandas_df = df.to_pandas(use_pyarrow_extension_array=True) 304 if json_cols: 305 import json 306 for col in json_cols: 307 pandas_df[col] = pandas_df[col].apply( 308 lambda value: json.loads(value) if isinstance(value, str) else None 309 ) 310 return pandas_df 311 312 313def to_polars( 314 df: Any, 315 geometry_cols_types_srids: Optional[Dict[str, Tuple[str, Any]]] = None, 316 json_cols: Optional[List[str]] = None, 317) -> Any: 318 """Convert a Pandas DataFrame to Polars; otherwise return ``df``.""" 319 if df.__class__.__module__.split('.')[0] == 'polars': 320 return df 321 polars = mrsm.attempt_import('polars') 322 geometry_cols_types_srids = ( 323 get_geometry_cols(df, with_types_srids=True) 324 if geometry_cols_types_srids is None and get_geometry_cols(df) 325 else (geometry_cols_types_srids or {}) 326 ) 327 geometry_cols_types_srids = { 328 col: type_srid 329 for col, type_srid in geometry_cols_types_srids.items() 330 if col in df.columns 331 } 332 json_cols = [col for col in (json_cols or []) if col in df.columns] 333 if geometry_cols_types_srids or json_cols: 334 try: 335 import json 336 geometry_cols = list(geometry_cols_types_srids) 337 special_cols = list(dict.fromkeys(geometry_cols + json_cols)) 338 polars_df = to_polars( 339 df.drop(columns=special_cols), 340 geometry_cols_types_srids={}, 341 json_cols=[], 342 ) 343 if geometry_cols: 344 from meerschaum.utils.dtypes import attempt_cast_to_geometry 345 shapely = mrsm.attempt_import('shapely', lazy=False) 346 ### Import inside the `mrsm` venv context: callers (e.g. `mrsm.entry()`) 347 ### may have deactivated it, and a bare import would then silently 348 ### skip registration so Arrow exports plain binary storage. 349 try: 350 geoarrow_pyarrow = mrsm.attempt_import( 351 'geoarrow.types.type_pyarrow', 352 lazy=False, 353 warn=False, 354 install=False, 355 ) 356 geoarrow_pyarrow.register_extension_types() 357 except Exception: 358 pass 359 for col, (_, srid) in geometry_cols_types_srids.items(): 360 crs = str(srid) if srid else None 361 if crs and ':' not in crs: 362 crs = 'EPSG:' + crs 363 metadata = json.dumps( 364 ({'crs': crs, 'crs_type': 'authority_code'} if crs else {}), 365 separators=(',', ':'), 366 ) 367 polars_df = polars_df.with_columns(polars.Series( 368 col, 369 shapely.to_wkb( 370 attempt_cast_to_geometry(df[col]), 371 hex=False, 372 include_srid=True, 373 ).tolist(), 374 dtype=polars.Extension('geoarrow.wkb', polars.Binary, metadata), 375 )) 376 if json_cols: 377 from meerschaum.utils.dtypes import json_serialize_value, value_is_null 378 for col in json_cols: 379 values = [] 380 for value in df[col].tolist(): 381 if value_is_null(value): 382 values.append(None) 383 continue 384 if isinstance(value, str): 385 try: 386 value = json.loads(value) 387 except json.JSONDecodeError: 388 pass 389 values.append(json.dumps( 390 value, 391 default=json_serialize_value, 392 separators=(',', ':'), 393 allow_nan=False, 394 )) 395 polars_df = polars_df.with_columns(polars.Series( 396 col, 397 values, 398 dtype=polars.Extension('arrow.json', polars.String, ''), 399 )) 400 return polars_df.select(list(df.columns)) 401 except Exception: 402 # ponytail: Preserve compatibility if Polars changes its unstable extension API. 403 pass 404 if get_uuid_cols(df): 405 return polars.DataFrame(df.to_dict(orient='list'), strict=False) 406 try: 407 return polars.from_pandas(df, include_index=False) 408 except (TypeError, ValueError): 409 # ponytail: Preserve unsupported Python objects; remove when Polars supports UUIDs. 410 return polars.DataFrame(df.to_dict(orient='list'), strict=False) 411 412 413def add_missing_cols_to_df( 414 df: 'pd.DataFrame', 415 dtypes: Dict[str, Any], 416) -> 'pd.DataFrame': 417 """ 418 Add columns from the dtypes dictionary as null columns to a new DataFrame. 419 420 Parameters 421 ---------- 422 df: pd.DataFrame 423 The dataframe we should copy and add null columns. 424 425 dtypes: 426 The data types dictionary which may contain keys not present in `df.columns`. 427 428 Returns 429 ------- 430 A new `DataFrame` with the keys from `dtypes` added as null columns. 431 If `df.dtypes` is the same as `dtypes`, then return a reference to `df`. 432 NOTE: This will not ensure that dtypes are enforced! 433 434 Examples 435 -------- 436 >>> import pandas as pd 437 >>> df = pd.DataFrame([{'a': 1}]) 438 >>> dtypes = {'b': 'Int64'} 439 >>> add_missing_cols_to_df(df, dtypes) 440 a b 441 0 1 <NA> 442 >>> add_missing_cols_to_df(df, dtypes).dtypes 443 a int64 444 b Int64 445 dtype: object 446 >>> add_missing_cols_to_df(df, {'a': 'object'}).dtypes 447 a int64 448 dtype: object 449 >>> 450 """ 451 if set(df.columns) == set(dtypes): 452 return df 453 454 from meerschaum.utils.packages import attempt_import 455 from meerschaum.utils.dtypes import to_pandas_dtype 456 pandas = attempt_import('pandas') 457 458 def build_series(dtype: str): 459 return pandas.Series([], dtype=to_pandas_dtype(dtype)) 460 461 assign_kwargs = { 462 str(col): build_series(str(typ)) 463 for col, typ in dtypes.items() 464 if col not in df.columns 465 } 466 df_with_cols = df.assign(**assign_kwargs) 467 for col in assign_kwargs: 468 df_with_cols[col] = df_with_cols[col].fillna(pandas.NA) 469 return df_with_cols 470 471 472def filter_unseen_df( 473 old_df: 'pd.DataFrame', 474 new_df: 'pd.DataFrame', 475 safe_copy: bool = True, 476 dtypes: Optional[Dict[str, Any]] = None, 477 include_unchanged_columns: bool = False, 478 coerce_mixed_numerics: bool = True, 479 debug: bool = False, 480) -> 'pd.DataFrame': 481 """ 482 Left join two DataFrames to find the newest unseen data. 483 484 Parameters 485 ---------- 486 old_df: 'pd.DataFrame' 487 The original (target) dataframe. Acts as a filter on the `new_df`. 488 489 new_df: 'pd.DataFrame' 490 The fetched (source) dataframe. Rows that are contained in `old_df` are removed. 491 492 safe_copy: bool, default True 493 If `True`, create a copy before comparing and modifying the dataframes. 494 Setting to `False` may mutate the DataFrames. 495 496 dtypes: Optional[Dict[str, Any]], default None 497 Optionally specify the datatypes of the dataframe. 498 499 include_unchanged_columns: bool, default False 500 If `True`, include columns which haven't changed on rows which have changed. 501 502 coerce_mixed_numerics: bool, default True 503 If `True`, cast mixed integer and float columns between the old and new dataframes into 504 numeric values (`decimal.Decimal`). 505 506 debug: bool, default False 507 Verbosity toggle. 508 509 Returns 510 ------- 511 A pandas dataframe of the new, unseen rows in `new_df`. 512 513 Examples 514 -------- 515 ```python 516 >>> import pandas as pd 517 >>> df1 = pd.DataFrame({'a': [1,2]}) 518 >>> df2 = pd.DataFrame({'a': [2,3]}) 519 >>> filter_unseen_df(df1, df2) 520 a 521 0 3 522 523 ``` 524 525 """ 526 _rust = _core.filter_unseen_df 527 return _rust( 528 old_df, new_df=new_df, safe_copy=safe_copy, dtypes=dtypes, 529 include_unchanged_columns=include_unchanged_columns, 530 coerce_mixed_numerics=coerce_mixed_numerics, debug=debug, 531 ) 532 533 534def parse_df_datetimes( 535 df: 'pd.DataFrame', 536 ignore_cols: Optional[Iterable[str]] = None, 537 strip_timezone: bool = False, 538 chunksize: Optional[int] = None, 539 dtype_backend: str = 'numpy_nullable', 540 ignore_all: bool = False, 541 precision_unit: Optional[str] = None, 542 coerce_utc: bool = True, 543 debug: bool = False, 544) -> 'pd.DataFrame': 545 """ 546 Parse a pandas DataFrame for datetime columns and cast as datetimes. 547 548 Parameters 549 ---------- 550 df: pd.DataFrame 551 The pandas DataFrame to parse. 552 553 ignore_cols: Optional[Iterable[str]], default None 554 If provided, do not attempt to coerce these columns as datetimes. 555 556 strip_timezone: bool, default False 557 If `True`, remove the UTC `tzinfo` property. 558 559 chunksize: Optional[int], default None 560 If the pandas implementation is `'dask'`, use this chunksize for the distributed dataframe. 561 562 dtype_backend: str, default 'numpy_nullable' 563 If `df` is not a DataFrame and new one needs to be constructed, 564 use this as the datatypes backend. 565 Accepted values are 'numpy_nullable' and 'pyarrow'. 566 567 ignore_all: bool, default False 568 If `True`, do not attempt to cast any columns to datetimes. 569 570 precision_unit: Optional[str], default None 571 If provided, enforce the given precision on the coerced datetime columns. 572 573 coerce_utc: bool, default True 574 Coerce the datetime columns to UTC (see `meerschaum.utils.dtypes.to_datetime()`). 575 576 debug: bool, default False 577 Verbosity toggle. 578 579 Returns 580 ------- 581 A new pandas DataFrame with the determined datetime columns 582 (usually ISO strings) cast as datetimes. 583 584 Examples 585 -------- 586 ```python 587 >>> import pandas as pd 588 >>> df = pd.DataFrame({'a': ['2022-01-01 00:00:00']}) 589 >>> df.dtypes 590 a object 591 dtype: object 592 >>> df2 = parse_df_datetimes(df) 593 >>> df2.dtypes 594 a datetime64[us, UTC] 595 dtype: object 596 597 ``` 598 599 """ 600 from meerschaum.utils.packages import import_pandas, attempt_import 601 from meerschaum.utils.debug import dprint 602 from meerschaum.utils.warnings import warn 603 from meerschaum.utils.misc import items_str 604 from meerschaum.utils.dtypes import to_datetime, MRSM_PD_DTYPES 605 import traceback 606 607 pd = import_pandas() 608 pandas = attempt_import('pandas') 609 pd_name = pd.__name__ 610 using_dask = 'dask' in pd_name 611 df_is_dask = (hasattr(df, '__module__') and 'dask' in df.__module__) 612 dask_dataframe = None 613 if using_dask or df_is_dask: 614 npartitions = chunksize_to_npartitions(chunksize) 615 dask_dataframe = attempt_import('dask.dataframe') 616 617 ### if df is a dict, build DataFrame 618 if isinstance(df, pandas.DataFrame): 619 pdf = df 620 elif df_is_dask and isinstance(df, dask_dataframe.DataFrame): 621 pdf = get_first_valid_dask_partition(df) 622 else: 623 if debug: 624 dprint(f"df is of type '{type(df)}'. Building {pd.DataFrame}...") 625 626 if using_dask: 627 if isinstance(df, list): 628 keys = set() 629 for doc in df: 630 for key in doc: 631 keys.add(key) 632 df = pd.DataFrame.from_dict( 633 { 634 k: [ 635 doc.get(k, None) 636 for doc in df 637 ] for k in keys 638 }, 639 npartitions=npartitions, 640 ) 641 elif isinstance(df, dict): 642 df = pd.DataFrame.from_dict(df, npartitions=npartitions) 643 elif 'pandas.core.frame.DataFrame' in str(type(df)): 644 df = pd.from_pandas(df, npartitions=npartitions) 645 else: 646 raise Exception("Can only parse dictionaries or lists of dictionaries with Dask.") 647 pandas = attempt_import('pandas') 648 pdf = get_first_valid_dask_partition(df) 649 650 else: 651 df = pd.DataFrame(df).convert_dtypes(dtype_backend=dtype_backend) 652 pdf = df 653 654 ### skip parsing if DataFrame is empty 655 if len(pdf) == 0: 656 if debug: 657 dprint("df is empty. Returning original DataFrame without casting datetime columns...") 658 return df 659 660 ignore_cols = set( 661 (ignore_cols or []) + [ 662 col 663 for col, dtype in pdf.dtypes.items() 664 if 'datetime' in str(dtype) 665 ] 666 ) 667 cols_to_inspect = [ 668 col 669 for col in pdf.columns 670 if col not in ignore_cols 671 ] if not ignore_all else [] 672 673 if len(cols_to_inspect) == 0: 674 if debug: 675 dprint("All columns are ignored, skipping datetime detection...") 676 return df.infer_objects().fillna(pandas.NA) 677 678 ### apply regex to columns to determine which are ISO datetimes 679 iso_dt_regex = r'\d{4}-\d{2}-\d{2}.\d{2}\:\d{2}\:\d+' 680 dt_mask = pdf[cols_to_inspect].astype(str).apply( 681 lambda s: s.str.match(iso_dt_regex).all() 682 ) 683 684 ### list of datetime column names 685 datetime_cols = [col for col in pdf[cols_to_inspect].loc[:, dt_mask]] 686 if not datetime_cols: 687 if debug: 688 dprint("No columns detected as datetimes, returning...") 689 return df.infer_objects().fillna(pandas.NA) 690 691 if debug: 692 dprint("Converting columns to datetimes: " + str(datetime_cols)) 693 694 def _parse_to_datetime(x): 695 return to_datetime(x, precision_unit=precision_unit, coerce_utc=coerce_utc) 696 697 try: 698 if not using_dask: 699 df[datetime_cols] = df[datetime_cols].apply(_parse_to_datetime) 700 else: 701 df[datetime_cols] = df[datetime_cols].apply( 702 _parse_to_datetime, 703 utc=True, 704 axis=1, 705 meta={ 706 col: MRSM_PD_DTYPES['datetime'] 707 for col in datetime_cols 708 } 709 ) 710 except Exception: 711 warn( 712 f"Unable to apply `to_datetime()` to {items_str(datetime_cols)}:\n" 713 + f"{traceback.format_exc()}" 714 ) 715 716 if strip_timezone: 717 for dt in datetime_cols: 718 try: 719 df[dt] = df[dt].dt.tz_localize(None) 720 except Exception: 721 warn( 722 f"Unable to convert column '{dt}' to naive datetime:\n" 723 + f"{traceback.format_exc()}" 724 ) 725 726 return df.fillna(pandas.NA) 727 728 729def get_unhashable_cols(df: 'pd.DataFrame') -> List[str]: 730 """ 731 Get the columns which contain unhashable objects from a Pandas DataFrame. 732 733 Parameters 734 ---------- 735 df: pd.DataFrame 736 The DataFrame which may contain unhashable objects. 737 738 Returns 739 ------- 740 A list of columns. 741 """ 742 if df is None: 743 return [] 744 if len(df) == 0: 745 return [] 746 747 is_dask = 'dask' in df.__module__ 748 if is_dask: 749 from meerschaum.utils.packages import attempt_import 750 pandas = attempt_import('pandas') 751 df = pandas.DataFrame(get_first_valid_dask_partition(df)) 752 return [ 753 col for col, val in df.iloc[0].items() 754 if not isinstance(val, Hashable) 755 ] 756 757 758def get_json_cols(df: 'pd.DataFrame') -> List[str]: 759 """ 760 Get the columns which contain unhashable objects from a Pandas DataFrame. 761 762 Parameters 763 ---------- 764 df: pd.DataFrame 765 The DataFrame which may contain unhashable objects. 766 767 Returns 768 ------- 769 A list of columns to be encoded as JSON. 770 """ 771 _rust = _core.get_json_cols 772 return _rust(df) 773 774 775def get_numeric_cols(df: 'pd.DataFrame') -> List[str]: 776 """ 777 Get the columns which contain `decimal.Decimal` objects from a Pandas DataFrame. 778 779 Parameters 780 ---------- 781 df: pd.DataFrame 782 The DataFrame which may contain decimal objects. 783 784 Returns 785 ------- 786 A list of columns to treat as numerics. 787 """ 788 _rust = _core.get_numeric_cols 789 return _rust(df) 790 791 792def get_bool_cols(df: 'pd.DataFrame') -> List[str]: 793 """ 794 Get the columns which contain `bool` objects from a Pandas DataFrame. 795 796 Parameters 797 ---------- 798 df: pd.DataFrame 799 The DataFrame which may contain bools. 800 801 Returns 802 ------- 803 A list of columns to treat as bools. 804 """ 805 if df is None: 806 return [] 807 808 is_dask = 'dask' in df.__module__ 809 if is_dask: 810 df = get_first_valid_dask_partition(df) 811 812 if len(df) == 0: 813 return [] 814 815 from meerschaum.utils.dtypes import are_dtypes_equal 816 817 return [ 818 col 819 for col, typ in df.dtypes.items() 820 if are_dtypes_equal(str(typ), 'bool') 821 ] 822 823 824def get_uuid_cols(df: 'pd.DataFrame') -> List[str]: 825 """ 826 Get the columns which contain `uuid.UUID` objects from a Pandas DataFrame. 827 828 Parameters 829 ---------- 830 df: pd.DataFrame 831 The DataFrame which may contain UUID objects. 832 833 Returns 834 ------- 835 A list of columns to treat as UUIDs. 836 """ 837 _rust = _core.get_uuid_cols 838 return _rust(df) 839 840 841def get_datetime_cols( 842 df: 'pd.DataFrame', 843 timezone_aware: bool = True, 844 timezone_naive: bool = True, 845 with_tz_precision: bool = False, 846) -> Union[List[str], Dict[str, Tuple[Union[str, None], str]]]: 847 """ 848 Get the columns which contain `datetime` or `Timestamp` objects from a Pandas DataFrame. 849 850 Parameters 851 ---------- 852 df: pd.DataFrame 853 The DataFrame which may contain `datetime` or `Timestamp` objects. 854 855 timezone_aware: bool, default True 856 If `True`, include timezone-aware datetime columns. 857 858 timezone_naive: bool, default True 859 If `True`, include timezone-naive datetime columns. 860 861 with_tz_precision: bool, default False 862 If `True`, return a dictionary mapping column names to tuples in the form 863 `(timezone, precision)`. 864 865 Returns 866 ------- 867 A list of columns to treat as datetimes, or a dictionary of columns to tz+precision tuples 868 (if `with_tz_precision` is `True`). 869 """ 870 _rust = _core.get_datetime_cols 871 return _rust( 872 df, timezone_aware=timezone_aware, timezone_naive=timezone_naive, 873 with_tz_precision=with_tz_precision, 874 ) 875 876 877def get_datetime_cols_types(df: 'pd.DataFrame') -> Dict[str, str]: 878 """ 879 Return a dictionary mapping datetime columns to specific types strings. 880 881 Parameters 882 ---------- 883 df: pd.DataFrame 884 The DataFrame which may contain datetime columns. 885 886 Returns 887 ------- 888 A dictionary mapping the datetime columns' names to dtype strings 889 (containing timezone and precision metadata). 890 891 Examples 892 -------- 893 >>> from datetime import datetime, timezone 894 >>> import pandas as pd 895 >>> df = pd.DataFrame({'dt_tz_aware': [datetime(2025, 1, 1, tzinfo=timezone.utc)]}) 896 >>> get_datetime_cols_types(df) 897 {'dt_tz_aware': 'datetime64[us, UTC]'} 898 >>> df = pd.DataFrame({'distant_dt': [datetime(1, 1, 1)]}) 899 >>> get_datetime_cols_types(df) 900 {'distant_dt': 'datetime64[us]'} 901 >>> df = pd.DataFrame({'dt_second': datetime(2025, 1, 1)}) 902 >>> df['dt_second'] = df['dt_second'].astype('datetime64[s]') 903 >>> get_datetime_cols_types(df) 904 {'dt_second': 'datetime64[s]'} 905 """ 906 from meerschaum.utils.dtypes import MRSM_PRECISION_UNITS_ABBREVIATIONS 907 dt_cols_tuples = get_datetime_cols(df, with_tz_precision=True) 908 if not dt_cols_tuples: 909 return {} 910 911 return { 912 col: ( 913 f"datetime64[{MRSM_PRECISION_UNITS_ABBREVIATIONS[precision]}]" 914 if tz is None 915 else f"datetime64[{MRSM_PRECISION_UNITS_ABBREVIATIONS[precision]}, {tz}]" 916 ) 917 for col, (tz, precision) in dt_cols_tuples.items() 918 } 919 920 921def get_date_cols(df: 'pd.DataFrame') -> List[str]: 922 """ 923 Get the `date` columns from a Pandas DataFrame. 924 925 Parameters 926 ---------- 927 df: pd.DataFrame 928 The DataFrame which may contain dates. 929 930 Returns 931 ------- 932 A list of columns to treat as dates. 933 """ 934 from meerschaum.utils.dtypes import are_dtypes_equal 935 if df is None: 936 return [] 937 938 is_dask = 'dask' in df.__module__ 939 if is_dask: 940 df = get_first_valid_dask_partition(df) 941 942 known_date_cols = [ 943 col 944 for col, typ in df.dtypes.items() 945 if are_dtypes_equal(typ, 'date') 946 ] 947 948 if len(df) == 0: 949 return known_date_cols 950 951 cols_indices = { 952 col: df[col].first_valid_index() 953 for col in df.columns 954 if col not in known_date_cols 955 } 956 object_date_cols = [ 957 col 958 for col, ix in cols_indices.items() 959 if ( 960 ix is not None 961 and isinstance(df.loc[ix][col], date) 962 ) 963 ] 964 965 all_date_cols = set(known_date_cols + object_date_cols) 966 967 return [ 968 col 969 for col in df.columns 970 if col in all_date_cols 971 ] 972 973 974def get_bytes_cols(df: 'pd.DataFrame') -> List[str]: 975 """ 976 Get the columns which contain bytes strings from a Pandas DataFrame. 977 978 Parameters 979 ---------- 980 df: pd.DataFrame 981 The DataFrame which may contain bytes strings. 982 983 Returns 984 ------- 985 A list of columns to treat as bytes. 986 """ 987 _rust = _core.get_bytes_cols 988 return _rust(df) 989 990 991def get_geometry_cols( 992 df: 'pd.DataFrame', 993 with_types_srids: bool = False, 994) -> Union[List[str], Dict[str, Any]]: 995 """ 996 Get the columns which contain shapely objects from a Pandas DataFrame. 997 998 Parameters 999 ---------- 1000 df: pd.DataFrame 1001 The DataFrame which may contain bytes strings. 1002 1003 with_types_srids: bool, default False 1004 If `True`, return a dictionary mapping columns to geometry types and SRIDs. 1005 1006 Returns 1007 ------- 1008 A list of columns to treat as `geometry`. 1009 If `with_types_srids`, return a dictionary mapping columns to tuples in the form (type, SRID). 1010 """ 1011 _rust = _core.get_geometry_cols 1012 return _rust(df, with_types_srids=with_types_srids) 1013 1014 1015def get_geometry_cols_types(df: 'pd.DataFrame') -> Dict[str, str]: 1016 """ 1017 Return a dtypes dictionary mapping columns to specific geometry types (type, srid). 1018 """ 1019 geometry_cols_types_srids = get_geometry_cols(df, with_types_srids=True) 1020 new_cols_types = {} 1021 for col, (geometry_type, srid) in geometry_cols_types_srids.items(): 1022 new_dtype = "geometry" 1023 modifier = "" 1024 if not srid and geometry_type.lower() == 'geometry': 1025 new_cols_types[col] = new_dtype 1026 continue 1027 1028 modifier = "[" 1029 if geometry_type.lower() != 'geometry': 1030 modifier += f"{geometry_type}" 1031 1032 if srid: 1033 if modifier != '[': 1034 modifier += ", " 1035 modifier += f"{srid}" 1036 modifier += "]" 1037 new_cols_types[col] = f"{new_dtype}{modifier}" 1038 return new_cols_types 1039 1040 1041def get_special_cols(df: 'pd.DataFrame') -> Dict[str, str]: 1042 """ 1043 Return a dtypes dictionary mapping special columns to their dtypes. 1044 """ 1045 return { 1046 **{col: 'json' for col in get_json_cols(df)}, 1047 **{col: 'uuid' for col in get_uuid_cols(df)}, 1048 **{col: 'bytes' for col in get_bytes_cols(df)}, 1049 **{col: 'bool' for col in get_bool_cols(df)}, 1050 **{col: 'numeric' for col in get_numeric_cols(df)}, 1051 **{col: 'date' for col in get_date_cols(df)}, 1052 **get_datetime_cols_types(df), 1053 **get_geometry_cols_types(df), 1054 } 1055 1056 1057def enforce_dtypes( 1058 df: 'pd.DataFrame', 1059 dtypes: Dict[str, str], 1060 explicit_dtypes: Optional[Dict[str, str]] = None, 1061 safe_copy: bool = True, 1062 coerce_numeric: bool = False, 1063 coerce_timezone: bool = True, 1064 strip_timezone: bool = False, 1065 as_polars: Optional[bool] = None, 1066 debug: bool = False, 1067 as_pandas: Optional[bool] = None, 1068) -> 'pd.DataFrame': 1069 """ 1070 Enforce the `dtypes` dictionary on a DataFrame. 1071 1072 Parameters 1073 ---------- 1074 df: pd.DataFrame 1075 The DataFrame on which to enforce dtypes. 1076 1077 dtypes: Dict[str, str] 1078 The data types to attempt to enforce on the DataFrame. 1079 1080 explicit_dtypes: Optional[Dict[str, str]], default None 1081 If provided, automatic dtype coersion will respect explicitly configured 1082 dtypes (`int`, `float`, `numeric`). 1083 1084 safe_copy: bool, default True 1085 If `True`, create a copy before comparing and modifying the dataframes. 1086 Setting to `False` may mutate the DataFrames. 1087 See `meerschaum.utils.dataframe.filter_unseen_df`. 1088 1089 coerce_numeric: bool, default False 1090 If `True`, convert float and int collisions to numeric. 1091 1092 coerce_timezone: bool, default True 1093 If `True`, convert datetimes to UTC. 1094 1095 strip_timezone: bool, default False 1096 If `coerce_timezone` and `strip_timezone` are `True`, 1097 remove timezone information from datetimes. 1098 1099 as_polars: Optional[bool], default None 1100 `True` returns a Polars DataFrame, `False` a pandas one. By default the result is the 1101 library `df` came in (a Polars frame stays Polars, a pandas frame pandas). 1102 1103 debug: bool, default False 1104 Verbosity toggle. 1105 1106 as_pandas: Optional[bool], default None 1107 `True` returns a pandas DataFrame, `False` a Polars one (see `as_polars`). 1108 1109 Returns 1110 ------- 1111 The DataFrame with the types enforced, of the library `df` came in (or the one asked for). 1112 """ 1113 if df is None: 1114 return df 1115 library = output_library_for(df, as_polars=as_polars, as_pandas=as_pandas) 1116 _rust = _core.enforce_dtypes 1117 return _rust( 1118 df, dtypes=dtypes, explicit_dtypes=explicit_dtypes, safe_copy=safe_copy, 1119 coerce_numeric=coerce_numeric, coerce_timezone=coerce_timezone, 1120 strip_timezone=strip_timezone, as_polars=(library == 'polars'), debug=debug, 1121 ) 1122 1123 1124def get_datetime_bound_from_df( 1125 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]], 1126 datetime_column: str, 1127 minimum: bool = True, 1128) -> Union[int, datetime, None]: 1129 """ 1130 Return the minimum or maximum datetime (or integer) from a DataFrame. 1131 1132 Parameters 1133 ---------- 1134 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]] 1135 The DataFrame, list, or dict which contains the range axis. 1136 1137 datetime_column: str 1138 The name of the datetime (or int) column. 1139 1140 minimum: bool 1141 Whether to return the minimum (default) or maximum value. 1142 1143 Returns 1144 ------- 1145 The minimum or maximum datetime value in the dataframe, or `None`. 1146 """ 1147 from meerschaum.utils.dtypes import to_datetime, value_is_null 1148 1149 if df is None: 1150 return None 1151 if not datetime_column: 1152 return None 1153 1154 def compare(a, b): 1155 if a is None: 1156 return b 1157 if b is None: 1158 return a 1159 if minimum: 1160 return a if a < b else b 1161 return a if a > b else b 1162 1163 if isinstance(df, list): 1164 if len(df) == 0: 1165 return None 1166 best_yet = df[0].get(datetime_column, None) 1167 for doc in df: 1168 val = doc.get(datetime_column, None) 1169 best_yet = compare(best_yet, val) 1170 return best_yet 1171 1172 if isinstance(df, dict): 1173 if datetime_column not in df: 1174 return None 1175 best_yet = df[datetime_column][0] 1176 for val in df[datetime_column]: 1177 best_yet = compare(best_yet, val) 1178 return best_yet 1179 1180 if 'DataFrame' in str(type(df)): 1181 from meerschaum.utils.dtypes import are_dtypes_equal 1182 pandas = mrsm.attempt_import('pandas') 1183 is_dask = 'dask' in df.__module__ 1184 1185 if datetime_column not in df.columns: 1186 return None 1187 1188 try: 1189 dt_val = ( 1190 df[datetime_column].min(skipna=True) 1191 if minimum 1192 else df[datetime_column].max(skipna=True) 1193 ) 1194 except Exception: 1195 dt_val = pandas.NA 1196 if is_dask and dt_val is not None and dt_val is not pandas.NA: 1197 dt_val = dt_val.compute() 1198 1199 return ( 1200 to_datetime(dt_val, as_pydatetime=True) 1201 if are_dtypes_equal(str(type(dt_val)), 'datetime') 1202 else (dt_val if not value_is_null(dt_val) else None) 1203 ) 1204 1205 return None 1206 1207 1208def get_unique_index_values( 1209 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]], 1210 indices: List[str], 1211) -> Dict[str, List[Any]]: 1212 """ 1213 Return a dictionary of the unique index values in a DataFrame. 1214 1215 Parameters 1216 ---------- 1217 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]] 1218 The dataframe (or list or dict) which contains index values. 1219 1220 indices: List[str] 1221 The list of index columns. 1222 1223 Returns 1224 ------- 1225 A dictionary mapping indices to unique values. 1226 """ 1227 if df is None: 1228 return {} 1229 if 'dataframe' in str(type(df)).lower(): 1230 pandas = mrsm.attempt_import('pandas') 1231 return { 1232 col: list({ 1233 (val if val is not pandas.NA else None) 1234 for val in df[col].unique() 1235 }) 1236 for col in indices 1237 if col in df.columns 1238 } 1239 1240 unique_indices = defaultdict(lambda: set()) 1241 if isinstance(df, list): 1242 for doc in df: 1243 for index in indices: 1244 if index in doc: 1245 unique_indices[index].add(doc[index]) 1246 1247 elif isinstance(df, dict): 1248 for index in indices: 1249 if index in df: 1250 unique_indices[index] = unique_indices[index].union(set(df[index])) 1251 1252 return {key: list(val) for key, val in unique_indices.items()} 1253 1254 1255def df_is_chunk_generator(df: Any) -> bool: 1256 """ 1257 Determine whether to treat `df` as a chunk generator. 1258 1259 Note this should only be used in a context where generators are expected, 1260 as it will return `True` for any iterable. 1261 1262 Parameters 1263 ---------- 1264 The DataFrame or chunk generator to evaluate. 1265 1266 Returns 1267 ------- 1268 A `bool` indicating whether to treat `df` as a generator. 1269 """ 1270 return ( 1271 not isinstance(df, (dict, list, str)) 1272 and 'DataFrame' not in str(type(df)) 1273 and isinstance(df, (Generator, Iterable, Iterator)) 1274 ) 1275 1276 1277def chunksize_to_npartitions(chunksize: Optional[int]) -> int: 1278 """ 1279 Return the Dask `npartitions` value for a given `chunksize`. 1280 """ 1281 if chunksize == -1: 1282 from meerschaum.config import get_config 1283 chunksize = get_config('system', 'connectors', 'sql', 'chunksize') 1284 if chunksize is None: 1285 return 1 1286 return -1 * chunksize 1287 1288 1289def df_from_literal( 1290 pipe: Optional[mrsm.Pipe] = None, 1291 literal: Optional[str] = None, 1292 debug: bool = False 1293) -> 'pd.DataFrame': 1294 """ 1295 Construct a dataframe from a literal value, using the pipe's datetime and value column names. 1296 1297 Parameters 1298 ---------- 1299 pipe: Optional['meerschaum.Pipe'], default None 1300 The pipe which will consume the literal value. 1301 1302 Returns 1303 ------- 1304 A 1-row pandas DataFrame from with the current UTC timestamp as the datetime columns 1305 and the literal as the value. 1306 """ 1307 from meerschaum.utils.packages import import_pandas 1308 from meerschaum.utils.warnings import error, warn 1309 from meerschaum.utils.debug import dprint 1310 from meerschaum.utils.dtypes import get_current_timestamp 1311 1312 if pipe is None or literal is None: 1313 error("Please provide a Pipe and a literal value") 1314 1315 dt_col = pipe.columns.get( 1316 'datetime', 1317 mrsm.get_config('pipes', 'autotime', 'column_name_if_datetime_missing') 1318 ) 1319 val_col = pipe.get_val_column(debug=debug) 1320 1321 val = literal 1322 if isinstance(literal, str): 1323 if debug: 1324 dprint(f"Received literal string: '{literal}'") 1325 import ast 1326 try: 1327 val = ast.literal_eval(literal) 1328 except Exception: 1329 warn( 1330 "Failed to parse value from string:\n" + f"{literal}" + 1331 "\n\nWill cast as a string instead."\ 1332 ) 1333 val = literal 1334 1335 now = get_current_timestamp(pipe.precision) 1336 pd = import_pandas() 1337 return pd.DataFrame({dt_col: [now], val_col: [val]}) 1338 1339 1340def get_first_valid_dask_partition(ddf: 'dask.dataframe.DataFrame') -> Union['pd.DataFrame', None]: 1341 """ 1342 Return the first valid Dask DataFrame partition (if possible). 1343 """ 1344 pdf = None 1345 for partition in ddf.partitions: 1346 try: 1347 pdf = partition.compute() 1348 except Exception: 1349 continue 1350 if len(pdf) > 0: 1351 return pdf 1352 _ = mrsm.attempt_import('partd', lazy=False) 1353 return ddf.compute() 1354 1355 1356def query_df( 1357 df: 'pd.DataFrame', 1358 params: Optional[Dict[str, Any]] = None, 1359 begin: Union[datetime, int, None] = None, 1360 end: Union[datetime, int, None] = None, 1361 datetime_column: Optional[str] = None, 1362 select_columns: Optional[List[str]] = None, 1363 omit_columns: Optional[List[str]] = None, 1364 inplace: bool = False, 1365 reset_index: bool = False, 1366 coerce_types: bool = False, 1367 debug: bool = False, 1368) -> 'pd.DataFrame': 1369 """ 1370 Query the dataframe with the params dictionary. 1371 1372 Parameters 1373 ---------- 1374 df: pd.DataFrame 1375 The DataFrame to query against. 1376 1377 params: Optional[Dict[str, Any]], default None 1378 The parameters dictionary to use for the query. 1379 1380 begin: Union[datetime, int, None], default None 1381 If `begin` and `datetime_column` are provided, only return rows with a timestamp 1382 greater than or equal to this value. 1383 1384 end: Union[datetime, int, None], default None 1385 If `begin` and `datetime_column` are provided, only return rows with a timestamp 1386 less than this value. 1387 1388 datetime_column: Optional[str], default None 1389 A `datetime_column` must be provided to use `begin` and `end`. 1390 1391 select_columns: Optional[List[str]], default None 1392 If provided, only return these columns. 1393 1394 omit_columns: Optional[List[str]], default None 1395 If provided, do not include these columns in the result. 1396 1397 inplace: bool, default False 1398 If `True`, modify the DataFrame inplace rather than creating a new DataFrame. 1399 1400 reset_index: bool, default False 1401 If `True`, reset the index in the resulting DataFrame. 1402 1403 coerce_types: bool, default False 1404 If `True`, cast the dataframe and parameters as strings before querying. 1405 1406 Returns 1407 ------- 1408 A Pandas DataFrame query result. 1409 """ 1410 _rust = _core.query_df 1411 return _rust( 1412 df, params=params, begin=begin, end=end, datetime_column=datetime_column, 1413 select_columns=select_columns, omit_columns=omit_columns, inplace=inplace, 1414 reset_index=reset_index, coerce_types=coerce_types, debug=debug, 1415 ) 1416 1417 1418def to_json( 1419 df: 'pd.DataFrame', 1420 safe_copy: bool = True, 1421 orient: str = 'records', 1422 date_format: str = 'iso', 1423 date_unit: str = 'us', 1424 double_precision: int = 15, 1425 geometry_format: str = 'geojson', 1426 **kwargs: Any 1427) -> str: 1428 """ 1429 Serialize the given dataframe as a JSON string. 1430 1431 Parameters 1432 ---------- 1433 df: pd.DataFrame 1434 The DataFrame to be serialized. 1435 1436 safe_copy: bool, default True 1437 If `False`, modify the DataFrame inplace. 1438 1439 date_format: str, default 'iso' 1440 The default format for timestamps. 1441 1442 date_unit: str, default 'us' 1443 The precision of the timestamps. 1444 1445 double_precision: int, default 15 1446 The number of decimal places to use when encoding floating point values (maximum 15). 1447 1448 geometry_format: str, default 'geojson' 1449 The serialization format for geometry data. 1450 Accepted values are `geojson`, `wkb_hex`, and `wkt`. 1451 1452 Returns 1453 ------- 1454 A JSON string. 1455 """ 1456 import warnings 1457 from meerschaum.utils.packages import import_pandas 1458 from meerschaum.utils.dtypes import ( 1459 serialize_bytes, 1460 serialize_decimal, 1461 serialize_geometry, 1462 serialize_date, 1463 ) 1464 pd = import_pandas() 1465 uuid_cols = get_uuid_cols(df) 1466 bytes_cols = get_bytes_cols(df) 1467 numeric_cols = get_numeric_cols(df) 1468 date_cols = get_date_cols(df) 1469 geometry_cols = get_geometry_cols(df) 1470 geometry_cols_srids = { 1471 col: int((getattr(df[col].crs, 'srs', '') or '').split(':', maxsplit=1)[-1] or '0') 1472 for col in geometry_cols 1473 } if 'geodataframe' in str(type(df)).lower() else {} 1474 if safe_copy and bool(uuid_cols or bytes_cols or geometry_cols or numeric_cols): 1475 df = df.copy() 1476 if 'geodataframe' in str(type(df)).lower(): 1477 geometry_data = { 1478 col: df[col] 1479 for col in geometry_cols 1480 } 1481 df = pd.DataFrame({ 1482 col: df[col] 1483 for col in df.columns 1484 if col not in geometry_cols 1485 }) 1486 for col in geometry_cols: 1487 df[col] = pd.Series(ob for ob in geometry_data[col]) 1488 for col in uuid_cols: 1489 df[col] = df[col].astype(str) 1490 for col in bytes_cols: 1491 df[col] = df[col].apply(serialize_bytes) 1492 for col in numeric_cols: 1493 df[col] = df[col].apply(serialize_decimal) 1494 for col in date_cols: 1495 df[col] = df[col].apply(serialize_date) 1496 with warnings.catch_warnings(): 1497 warnings.simplefilter("ignore") 1498 for col in geometry_cols: 1499 srid = geometry_cols_srids.get(col, None) or None 1500 df[col] = pd.Series( 1501 serialize_geometry(val, geometry_format=geometry_format, srid=srid) 1502 for val in df[col] 1503 ) 1504 return df.infer_objects().fillna(pd.NA).to_json( 1505 date_format=date_format, 1506 date_unit=date_unit, 1507 double_precision=double_precision, 1508 orient=orient, 1509 **kwargs 1510 ) 1511 1512 1513def to_simple_lines(df: 'pd.DataFrame') -> str: 1514 """ 1515 Serialize a Pandas Dataframe as lines of simple dictionaries. 1516 1517 Parameters 1518 ---------- 1519 df: pd.DataFrame 1520 The dataframe to serialize into simple lines text. 1521 1522 Returns 1523 ------- 1524 A string of simple line dictionaries joined by newlines. 1525 """ 1526 from meerschaum.utils.misc import to_simple_dict 1527 if df is None or len(df) == 0: 1528 return '' 1529 1530 docs = df.to_dict(orient='records') 1531 return '\n'.join(to_simple_dict(doc) for doc in docs) 1532 1533 1534def parse_simple_lines(data: str) -> 'pd.DataFrame': 1535 """ 1536 Parse simple lines text into a DataFrame. 1537 1538 Parameters 1539 ---------- 1540 data: str 1541 The simple lines text to parse into a DataFrame. 1542 1543 Returns 1544 ------- 1545 A dataframe containing the rows serialized in `data`. 1546 """ 1547 from meerschaum.utils.misc import string_to_dict 1548 from meerschaum.utils.packages import import_pandas 1549 pd = import_pandas() 1550 lines = data.splitlines() 1551 try: 1552 docs = [string_to_dict(line) for line in lines] 1553 df = pd.DataFrame(docs) 1554 except Exception: 1555 df = None 1556 1557 if df is None: 1558 raise ValueError("Cannot parse simple lines into a dataframe.") 1559 1560 return df
54def get_dataframe_library() -> str: 55 """ 56 Return the DataFrame library Meerschaum returns frames in: `'polars'` or `'pandas'`. 57 58 The setting is `dataframe` in `meerschaum.yaml` (config path `meerschaum:dataframe`), and the 59 `MRSM_DATAFRAME` environment variable overrides it for one process (and the jobs it starts). 60 A fresh root writes `dataframe: polars`; a root created by Meerschaum 4 or by an earlier 5.0 61 release candidate is given `dataframe: pandas` the first time 5.0.0rc15 or newer opens it. 62 63 Every API that returns a DataFrame follows it (`Pipe.get_data()`, `SQLConnector.read()`, 64 `Pipe.fetch()`, `Plan.to_dataframe()`, the frames handed to plugin code, ...), and 65 `as_polars=True` / `as_pandas=True` override it for one call. 66 67 Returns 68 ------- 69 `'polars'` or `'pandas'`. 70 71 Examples 72 -------- 73 >>> import os 74 >>> os.environ['MRSM_DATAFRAME'] = 'pandas' 75 >>> get_dataframe_library() 76 'pandas' 77 """ 78 import os 79 value = os.environ.get(DATAFRAME_ENV_VAR, None) 80 if value is not None and value.strip(): 81 return _normalize_dataframe_library(value, DATAFRAME_ENV_VAR) 82 from meerschaum.config import get_config 83 return _normalize_dataframe_library( 84 get_config('meerschaum', 'dataframe', warn=False), 85 'meerschaum:dataframe', 86 )
Return the DataFrame library Meerschaum returns frames in: 'polars' or 'pandas'.
The setting is dataframe in meerschaum.yaml (config path meerschaum:dataframe), and the
MRSM_DATAFRAME environment variable overrides it for one process (and the jobs it starts).
A fresh root writes dataframe: polars; a root created by Meerschaum 4 or by an earlier 5.0
release candidate is given dataframe: pandas the first time 5.0.0rc15 or newer opens it.
Every API that returns a DataFrame follows it (Pipe.get_data(), SQLConnector.read(),
Pipe.fetch(), Plan.to_dataframe(), the frames handed to plugin code, ...), and
as_polars=True / as_pandas=True override it for one call.
Returns
'polars'or'pandas'.
Examples
>>> import os
>>> os.environ['MRSM_DATAFRAME'] = 'pandas'
>>> get_dataframe_library()
'pandas'
89def resolve_dataframe_library( 90 as_polars: Optional[bool] = None, 91 as_pandas: Optional[bool] = None, 92) -> str: 93 """ 94 Return the library one call returns its frames in: `'polars'` or `'pandas'`. 95 96 Parameters 97 ---------- 98 as_polars: Optional[bool], default None 99 `True` asks for Polars and `False` for pandas (Meerschaum 4's meaning of 100 `as_polars=False`). `None` follows `get_dataframe_library()`. 101 102 as_pandas: Optional[bool], default None 103 `True` asks for pandas and `False` for Polars. `None` follows the setting. 104 105 Returns 106 ------- 107 `'polars'` or `'pandas'`. Asking for both (or for neither) raises a `ValueError`. 108 """ 109 wants_polars = None if as_polars is None else bool(as_polars) 110 wants_pandas = None if as_pandas is None else bool(as_pandas) 111 if wants_polars is not None and wants_pandas is not None and wants_polars == wants_pandas: 112 raise ValueError( 113 "`as_polars` and `as_pandas` contradict each other: pass only one of them." 114 ) 115 if wants_polars is not None: 116 return 'polars' if wants_polars else 'pandas' 117 if wants_pandas is not None: 118 return 'pandas' if wants_pandas else 'polars' 119 return get_dataframe_library()
Return the library one call returns its frames in: 'polars' or 'pandas'.
Parameters
- as_polars (Optional[bool], default None):
Trueasks for Polars andFalsefor pandas (Meerschaum 4's meaning ofas_polars=False).Nonefollowsget_dataframe_library(). - as_pandas (Optional[bool], default None):
Trueasks for pandas andFalsefor Polars.Nonefollows the setting.
Returns
'polars'or'pandas'. Asking for both (or for neither) raises aValueError.
122def wants_polars( 123 as_polars: Optional[bool] = None, 124 as_pandas: Optional[bool] = None, 125) -> bool: 126 """Return whether one call returns Polars frames (see `resolve_dataframe_library()`).""" 127 return resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) == 'polars'
Return whether one call returns Polars frames (see resolve_dataframe_library()).
130def import_dataframe_library(library: Optional[str] = None) -> Any: 131 """ 132 Import the `polars` or `pandas` module (the configured one by default). 133 134 A missing library is installed into the `mrsm` virtual environment when automatic installs are 135 allowed (as for any other optional package); otherwise the `ImportError` names the extra to 136 install (`pip install meerschaum[polars]` or `pip install meerschaum[pandas]`). 137 """ 138 library = library or get_dataframe_library() 139 from meerschaum.utils.packages import attempt_import, import_pandas 140 module = None 141 try: 142 if library == 'pandas': 143 module = import_pandas(warn=False) 144 else: 145 module = attempt_import(library, lazy=False, warn=False) 146 except Exception: 147 module = None 148 if module is None: 149 raise missing_dataframe_library_error(library) 150 return module
Import the polars or pandas module (the configured one by default).
A missing library is installed into the mrsm virtual environment when automatic installs are
allowed (as for any other optional package); otherwise the ImportError names the extra to
install (pip install meerschaum[polars] or pip install meerschaum[pandas]).
153def ensure_dataframe_library(library: Optional[str] = None) -> Any: 154 """ 155 Return the `polars` or `pandas` module (the configured one by default), importing it on the 156 first call (`import_dataframe_library()`: installed on first use when automatic installs 157 are allowed, else an `ImportError` naming the extra). Cheap once the module is imported. 158 """ 159 import sys 160 library = library or get_dataframe_library() 161 module = sys.modules.get(library, None) 162 if module is not None and not getattr(getattr(module, '__spec__', None), '_initializing', False): 163 if library != 'pandas' or 'pyarrow' in sys.modules: 164 return module 165 return import_dataframe_library(library)
Return the polars or pandas module (the configured one by default), importing it on the
first call (import_dataframe_library(): installed on first use when automatic installs
are allowed, else an ImportError naming the extra). Cheap once the module is imported.
168def missing_dataframe_library_error(library: str) -> ImportError: 169 """The `ImportError` for a DataFrame library that is not installed (names the extra).""" 170 extra = DATAFRAME_EXTRAS.get(library, library) 171 other = 'pandas' if library == 'polars' else 'polars' 172 return ImportError( 173 f"Meerschaum returns {library} DataFrames here, but {library} is not installed. " 174 f"Install it with `pip install meerschaum[{extra}]`, " 175 f"or set `dataframe: {other}` in meerschaum.yaml (`MRSM_DATAFRAME={other}`)." 176 )
The ImportError for a DataFrame library that is not installed (names the extra).
179def is_polars_frame(df: Any) -> bool: 180 """Whether `df` is a Polars DataFrame or LazyFrame.""" 181 return (type(df).__module__ or '').split('.')[0] == 'polars'
Whether df is a Polars DataFrame or LazyFrame.
184def is_pandas_frame(df: Any) -> bool: 185 """Whether `df` is a pandas (or GeoPandas) DataFrame.""" 186 return ( 187 (type(df).__module__ or '').split('.')[0] in ('pandas', 'geopandas') 188 and hasattr(df, 'iloc') 189 )
Whether df is a pandas (or GeoPandas) DataFrame.
192def frame_library_of(df: Any) -> Optional[str]: 193 """`'polars'`, `'pandas'` or `None`: the library of a DataFrame (or `None` for other data).""" 194 if is_polars_frame(df): 195 return 'polars' 196 if is_pandas_frame(df): 197 return 'pandas' 198 return None
'polars', 'pandas' or None: the library of a DataFrame (or None for other data).
201def output_library_for( 202 df: Any, 203 as_polars: Optional[bool] = None, 204 as_pandas: Optional[bool] = None, 205) -> str: 206 """ 207 The library a function that transforms a frame returns: what the call asks for 208 (`as_polars` / `as_pandas`), else the library `df` came in (a Polars frame stays Polars, a 209 pandas frame pandas), else (documents, dictionaries, Arrow data) `get_dataframe_library()`. 210 """ 211 if as_polars is not None or as_pandas is not None: 212 return resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 213 return frame_library_of(df) or get_dataframe_library()
The library a function that transforms a frame returns: what the call asks for
(as_polars / as_pandas), else the library df came in (a Polars frame stays Polars, a
pandas frame pandas), else (documents, dictionaries, Arrow data) get_dataframe_library().
216def frame_from_arrow( 217 data: Any, 218 *, 219 as_polars: Optional[bool] = None, 220 as_pandas: Optional[bool] = None, 221 dtypes: Optional[Dict[str, str]] = None, 222 debug: bool = False, 223) -> Any: 224 """ 225 Build the configured library's DataFrame from Arrow data (the one conversion every 226 frame-returning API uses). 227 228 Parameters 229 ---------- 230 data: Any 231 Anything that implements the Arrow PyCapsule interface (`__arrow_c_stream__` or 232 `__arrow_c_array__`): a `meerschaum._core.ArrowStream`, a `pyarrow.Table`, 233 `RecordBatch` or `RecordBatchReader`, ... or a list (or tuple) of record batches with one 234 schema. A pandas or Polars DataFrame is converted to the requested library. 235 `None` returns `None`. 236 237 as_polars: Optional[bool], default None 238 `True` returns Polars, `False` pandas; `None` follows `get_dataframe_library()`. 239 240 as_pandas: Optional[bool], default None 241 `True` returns pandas, `False` Polars; `None` follows the setting. 242 243 dtypes: Optional[Dict[str, str]], default None 244 Per-column dtypes: Meerschaum dtype strings (`'int'`, `'json'`, `'geometry[Point, 4326]'`, 245 ...) or pandas dtype strings (`'Int64'`, `'int64[pyarrow]'`, ...). 246 For pandas they restore the exact pandas dtype within the column's kind, as every read of 247 the façade does (`int64` vs `Int64` vs `int64[pyarrow]`, the datetime unit); a dtype of 248 another kind does not cast the column. For Polars, declared `json` and `geometry` 249 columns become extension columns even when every value is null. 250 251 debug: bool, default False 252 Verbosity toggle. 253 254 Returns 255 ------- 256 A `polars.DataFrame` or a `pandas.DataFrame` (a `geopandas.GeoDataFrame` when there are 257 geometry columns): JSON, UUID, numeric and geometry columns are restored from the Arrow 258 field metadata the kernel writes (`ARROW:extension:name`, `meerschaum:dtype`). Polars frames 259 carry geometry as GeoArrow WKB (binary) with the CRS in the field metadata. 260 261 Examples 262 -------- 263 >>> import pyarrow as pa 264 >>> frame_from_arrow(pa.table({'a': [1, None]}), as_pandas=True).dtypes 265 a Int64 266 dtype: object 267 >>> frame_from_arrow(pa.table({'a': [1, None]}), as_polars=True).schema 268 Schema([('a', Int64)]) 269 """ 270 if data is None: 271 return None 272 library = resolve_dataframe_library(as_polars=as_polars, as_pandas=as_pandas) 273 to_polars_frame = library == 'polars' 274 current = frame_library_of(data) 275 if current is not None: 276 if current == library: 277 return data 278 return to_polars(data) if to_polars_frame else to_pandas(data) 279 ensure_dataframe_library(library) 280 if isinstance(data, (list, tuple)): 281 data = list(data) 282 _rust = _core.frame_from_arrow 283 return _rust( 284 data, 285 as_polars=to_polars_frame, 286 dtypes=({str(col): str(typ) for col, typ in dtypes.items()} if dtypes else None), 287 debug=debug, 288 )
Build the configured library's DataFrame from Arrow data (the one conversion every frame-returning API uses).
Parameters
- data (Any):
Anything that implements the Arrow PyCapsule interface (
__arrow_c_stream__or__arrow_c_array__): ameerschaum._core.ArrowStream, apyarrow.Table,RecordBatchorRecordBatchReader, ... or a list (or tuple) of record batches with one schema. A pandas or Polars DataFrame is converted to the requested library.NonereturnsNone. - as_polars (Optional[bool], default None):
Truereturns Polars,Falsepandas;Nonefollowsget_dataframe_library(). - as_pandas (Optional[bool], default None):
Truereturns pandas,FalsePolars;Nonefollows the setting. - dtypes (Optional[Dict[str, str]], default None):
Per-column dtypes: Meerschaum dtype strings (
'int','json','geometry[Point, 4326]', ...) or pandas dtype strings ('Int64','int64[pyarrow]', ...). For pandas they restore the exact pandas dtype within the column's kind, as every read of the façade does (int64vsInt64vsint64[pyarrow], the datetime unit); a dtype of another kind does not cast the column. For Polars, declaredjsonandgeometrycolumns become extension columns even when every value is null. - debug (bool, default False): Verbosity toggle.
Returns
- A
polars.DataFrameor apandas.DataFrame(ageopandas.GeoDataFramewhen there are geometry columns) (JSON, UUID, numeric and geometry columns are restored from the Arrow):
**field metadata the kernel writes (
ARROW:extension:name,meerschaum** (dtype). Polars frames):carry geometry as GeoArrow WKB (binary) with the CRS in the field metadata.
Examples
>>> import pyarrow as pa
>>> frame_from_arrow(pa.table({'a': [1, None]}), as_pandas=True)meerschaum.utils.dtypes
a Int64
dtype: object
>>> frame_from_arrow(pa.table({'a': [1, None]}), as_polars=True).schema
Schema([('a', Int64)])
291def to_pandas(df: Any) -> Any: 292 """Convert a Polars DataFrame or LazyFrame to Pandas; otherwise return ``df``.""" 293 if df.__class__.__module__.split('.')[0] != 'polars': 294 return df 295 if df.__class__.__name__ == 'LazyFrame': 296 df = df.collect() 297 json_cols = [ 298 col 299 for col, typ in df.schema.items() 300 if getattr(typ, 'ext_name', lambda: None)() == 'arrow.json' 301 ] 302 if json_cols: 303 df = df.with_columns(df[col].ext.storage() for col in json_cols) 304 pandas_df = df.to_pandas(use_pyarrow_extension_array=True) 305 if json_cols: 306 import json 307 for col in json_cols: 308 pandas_df[col] = pandas_df[col].apply( 309 lambda value: json.loads(value) if isinstance(value, str) else None 310 ) 311 return pandas_df
Convert a Polars DataFrame or LazyFrame to Pandas; otherwise return df.
314def to_polars( 315 df: Any, 316 geometry_cols_types_srids: Optional[Dict[str, Tuple[str, Any]]] = None, 317 json_cols: Optional[List[str]] = None, 318) -> Any: 319 """Convert a Pandas DataFrame to Polars; otherwise return ``df``.""" 320 if df.__class__.__module__.split('.')[0] == 'polars': 321 return df 322 polars = mrsm.attempt_import('polars') 323 geometry_cols_types_srids = ( 324 get_geometry_cols(df, with_types_srids=True) 325 if geometry_cols_types_srids is None and get_geometry_cols(df) 326 else (geometry_cols_types_srids or {}) 327 ) 328 geometry_cols_types_srids = { 329 col: type_srid 330 for col, type_srid in geometry_cols_types_srids.items() 331 if col in df.columns 332 } 333 json_cols = [col for col in (json_cols or []) if col in df.columns] 334 if geometry_cols_types_srids or json_cols: 335 try: 336 import json 337 geometry_cols = list(geometry_cols_types_srids) 338 special_cols = list(dict.fromkeys(geometry_cols + json_cols)) 339 polars_df = to_polars( 340 df.drop(columns=special_cols), 341 geometry_cols_types_srids={}, 342 json_cols=[], 343 ) 344 if geometry_cols: 345 from meerschaum.utils.dtypes import attempt_cast_to_geometry 346 shapely = mrsm.attempt_import('shapely', lazy=False) 347 ### Import inside the `mrsm` venv context: callers (e.g. `mrsm.entry()`) 348 ### may have deactivated it, and a bare import would then silently 349 ### skip registration so Arrow exports plain binary storage. 350 try: 351 geoarrow_pyarrow = mrsm.attempt_import( 352 'geoarrow.types.type_pyarrow', 353 lazy=False, 354 warn=False, 355 install=False, 356 ) 357 geoarrow_pyarrow.register_extension_types() 358 except Exception: 359 pass 360 for col, (_, srid) in geometry_cols_types_srids.items(): 361 crs = str(srid) if srid else None 362 if crs and ':' not in crs: 363 crs = 'EPSG:' + crs 364 metadata = json.dumps( 365 ({'crs': crs, 'crs_type': 'authority_code'} if crs else {}), 366 separators=(',', ':'), 367 ) 368 polars_df = polars_df.with_columns(polars.Series( 369 col, 370 shapely.to_wkb( 371 attempt_cast_to_geometry(df[col]), 372 hex=False, 373 include_srid=True, 374 ).tolist(), 375 dtype=polars.Extension('geoarrow.wkb', polars.Binary, metadata), 376 )) 377 if json_cols: 378 from meerschaum.utils.dtypes import json_serialize_value, value_is_null 379 for col in json_cols: 380 values = [] 381 for value in df[col].tolist(): 382 if value_is_null(value): 383 values.append(None) 384 continue 385 if isinstance(value, str): 386 try: 387 value = json.loads(value) 388 except json.JSONDecodeError: 389 pass 390 values.append(json.dumps( 391 value, 392 default=json_serialize_value, 393 separators=(',', ':'), 394 allow_nan=False, 395 )) 396 polars_df = polars_df.with_columns(polars.Series( 397 col, 398 values, 399 dtype=polars.Extension('arrow.json', polars.String, ''), 400 )) 401 return polars_df.select(list(df.columns)) 402 except Exception: 403 # ponytail: Preserve compatibility if Polars changes its unstable extension API. 404 pass 405 if get_uuid_cols(df): 406 return polars.DataFrame(df.to_dict(orient='list'), strict=False) 407 try: 408 return polars.from_pandas(df, include_index=False) 409 except (TypeError, ValueError): 410 # ponytail: Preserve unsupported Python objects; remove when Polars supports UUIDs. 411 return polars.DataFrame(df.to_dict(orient='list'), strict=False)
Convert a Pandas DataFrame to Polars; otherwise return df.
414def add_missing_cols_to_df( 415 df: 'pd.DataFrame', 416 dtypes: Dict[str, Any], 417) -> 'pd.DataFrame': 418 """ 419 Add columns from the dtypes dictionary as null columns to a new DataFrame. 420 421 Parameters 422 ---------- 423 df: pd.DataFrame 424 The dataframe we should copy and add null columns. 425 426 dtypes: 427 The data types dictionary which may contain keys not present in `df.columns`. 428 429 Returns 430 ------- 431 A new `DataFrame` with the keys from `dtypes` added as null columns. 432 If `df.dtypes` is the same as `dtypes`, then return a reference to `df`. 433 NOTE: This will not ensure that dtypes are enforced! 434 435 Examples 436 -------- 437 >>> import pandas as pd 438 >>> df = pd.DataFrame([{'a': 1}]) 439 >>> dtypes = {'b': 'Int64'} 440 >>> add_missing_cols_to_df(df, dtypes) 441 a b 442 0 1 <NA> 443 >>> add_missing_cols_to_df(df, dtypes).dtypes 444 a int64 445 b Int64 446 dtype: object 447 >>> add_missing_cols_to_df(df, {'a': 'object'}).dtypes 448 a int64 449 dtype: object 450 >>> 451 """ 452 if set(df.columns) == set(dtypes): 453 return df 454 455 from meerschaum.utils.packages import attempt_import 456 from meerschaum.utils.dtypes import to_pandas_dtype 457 pandas = attempt_import('pandas') 458 459 def build_series(dtype: str): 460 return pandas.Series([], dtype=to_pandas_dtype(dtype)) 461 462 assign_kwargs = { 463 str(col): build_series(str(typ)) 464 for col, typ in dtypes.items() 465 if col not in df.columns 466 } 467 df_with_cols = df.assign(**assign_kwargs) 468 for col in assign_kwargs: 469 df_with_cols[col] = df_with_cols[col].fillna(pandas.NA) 470 return df_with_cols
Add columns from the dtypes dictionary as null columns to a new DataFrame.
Parameters
- df (pd.DataFrame): The dataframe we should copy and add null columns.
- dtypes:: The data types dictionary which may contain keys not present in
df.columns.
Returns
- A new
DataFramewith the keys fromdtypesadded as null columns. - If
df.dtypesis the same asdtypes, then return a reference todf. - NOTE (This will not ensure that dtypes are enforced!):
Examples
>>> import pandas as pd
>>> df = pd.DataFrame([{'a': 1}])
>>> dtypes = {'b': 'Int64'}
>>> add_missing_cols_to_df(df, dtypes)
a b
0 1 <NA>
>>> add_missing_cols_to_df(df, dtypes)meerschaum.utils.dtypes
a int64
b Int64
dtype: object
>>> add_missing_cols_to_df(df, {'a': 'object'})meerschaum.utils.dtypes
a int64
dtype: object
>>>
473def filter_unseen_df( 474 old_df: 'pd.DataFrame', 475 new_df: 'pd.DataFrame', 476 safe_copy: bool = True, 477 dtypes: Optional[Dict[str, Any]] = None, 478 include_unchanged_columns: bool = False, 479 coerce_mixed_numerics: bool = True, 480 debug: bool = False, 481) -> 'pd.DataFrame': 482 """ 483 Left join two DataFrames to find the newest unseen data. 484 485 Parameters 486 ---------- 487 old_df: 'pd.DataFrame' 488 The original (target) dataframe. Acts as a filter on the `new_df`. 489 490 new_df: 'pd.DataFrame' 491 The fetched (source) dataframe. Rows that are contained in `old_df` are removed. 492 493 safe_copy: bool, default True 494 If `True`, create a copy before comparing and modifying the dataframes. 495 Setting to `False` may mutate the DataFrames. 496 497 dtypes: Optional[Dict[str, Any]], default None 498 Optionally specify the datatypes of the dataframe. 499 500 include_unchanged_columns: bool, default False 501 If `True`, include columns which haven't changed on rows which have changed. 502 503 coerce_mixed_numerics: bool, default True 504 If `True`, cast mixed integer and float columns between the old and new dataframes into 505 numeric values (`decimal.Decimal`). 506 507 debug: bool, default False 508 Verbosity toggle. 509 510 Returns 511 ------- 512 A pandas dataframe of the new, unseen rows in `new_df`. 513 514 Examples 515 -------- 516 ```python 517 >>> import pandas as pd 518 >>> df1 = pd.DataFrame({'a': [1,2]}) 519 >>> df2 = pd.DataFrame({'a': [2,3]}) 520 >>> filter_unseen_df(df1, df2) 521 a 522 0 3 523 524 ``` 525 526 """ 527 _rust = _core.filter_unseen_df 528 return _rust( 529 old_df, new_df=new_df, safe_copy=safe_copy, dtypes=dtypes, 530 include_unchanged_columns=include_unchanged_columns, 531 coerce_mixed_numerics=coerce_mixed_numerics, debug=debug, 532 )
Left join two DataFrames to find the newest unseen data.
Parameters
- old_df ('pd.DataFrame'):
The original (target) dataframe. Acts as a filter on the
new_df. - new_df ('pd.DataFrame'):
The fetched (source) dataframe. Rows that are contained in
old_dfare removed. - safe_copy (bool, default True):
If
True, create a copy before comparing and modifying the dataframes. Setting toFalsemay mutate the DataFrames. - dtypes (Optional[Dict[str, Any]], default None): Optionally specify the datatypes of the dataframe.
- include_unchanged_columns (bool, default False):
If
True, include columns which haven't changed on rows which have changed. - coerce_mixed_numerics (bool, default True):
If
True, cast mixed integer and float columns between the old and new dataframes into numeric values (decimal.Decimal). - debug (bool, default False): Verbosity toggle.
Returns
- A pandas dataframe of the new, unseen rows in
new_df.
Examples
>>> import pandas as pd
>>> df1 = pd.DataFrame({'a': [1,2]})
>>> df2 = pd.DataFrame({'a': [2,3]})
>>> filter_unseen_df(df1, df2)
a
0 3
535def parse_df_datetimes( 536 df: 'pd.DataFrame', 537 ignore_cols: Optional[Iterable[str]] = None, 538 strip_timezone: bool = False, 539 chunksize: Optional[int] = None, 540 dtype_backend: str = 'numpy_nullable', 541 ignore_all: bool = False, 542 precision_unit: Optional[str] = None, 543 coerce_utc: bool = True, 544 debug: bool = False, 545) -> 'pd.DataFrame': 546 """ 547 Parse a pandas DataFrame for datetime columns and cast as datetimes. 548 549 Parameters 550 ---------- 551 df: pd.DataFrame 552 The pandas DataFrame to parse. 553 554 ignore_cols: Optional[Iterable[str]], default None 555 If provided, do not attempt to coerce these columns as datetimes. 556 557 strip_timezone: bool, default False 558 If `True`, remove the UTC `tzinfo` property. 559 560 chunksize: Optional[int], default None 561 If the pandas implementation is `'dask'`, use this chunksize for the distributed dataframe. 562 563 dtype_backend: str, default 'numpy_nullable' 564 If `df` is not a DataFrame and new one needs to be constructed, 565 use this as the datatypes backend. 566 Accepted values are 'numpy_nullable' and 'pyarrow'. 567 568 ignore_all: bool, default False 569 If `True`, do not attempt to cast any columns to datetimes. 570 571 precision_unit: Optional[str], default None 572 If provided, enforce the given precision on the coerced datetime columns. 573 574 coerce_utc: bool, default True 575 Coerce the datetime columns to UTC (see `meerschaum.utils.dtypes.to_datetime()`). 576 577 debug: bool, default False 578 Verbosity toggle. 579 580 Returns 581 ------- 582 A new pandas DataFrame with the determined datetime columns 583 (usually ISO strings) cast as datetimes. 584 585 Examples 586 -------- 587 ```python 588 >>> import pandas as pd 589 >>> df = pd.DataFrame({'a': ['2022-01-01 00:00:00']}) 590 >>> df.dtypes 591 a object 592 dtype: object 593 >>> df2 = parse_df_datetimes(df) 594 >>> df2.dtypes 595 a datetime64[us, UTC] 596 dtype: object 597 598 ``` 599 600 """ 601 from meerschaum.utils.packages import import_pandas, attempt_import 602 from meerschaum.utils.debug import dprint 603 from meerschaum.utils.warnings import warn 604 from meerschaum.utils.misc import items_str 605 from meerschaum.utils.dtypes import to_datetime, MRSM_PD_DTYPES 606 import traceback 607 608 pd = import_pandas() 609 pandas = attempt_import('pandas') 610 pd_name = pd.__name__ 611 using_dask = 'dask' in pd_name 612 df_is_dask = (hasattr(df, '__module__') and 'dask' in df.__module__) 613 dask_dataframe = None 614 if using_dask or df_is_dask: 615 npartitions = chunksize_to_npartitions(chunksize) 616 dask_dataframe = attempt_import('dask.dataframe') 617 618 ### if df is a dict, build DataFrame 619 if isinstance(df, pandas.DataFrame): 620 pdf = df 621 elif df_is_dask and isinstance(df, dask_dataframe.DataFrame): 622 pdf = get_first_valid_dask_partition(df) 623 else: 624 if debug: 625 dprint(f"df is of type '{type(df)}'. Building {pd.DataFrame}...") 626 627 if using_dask: 628 if isinstance(df, list): 629 keys = set() 630 for doc in df: 631 for key in doc: 632 keys.add(key) 633 df = pd.DataFrame.from_dict( 634 { 635 k: [ 636 doc.get(k, None) 637 for doc in df 638 ] for k in keys 639 }, 640 npartitions=npartitions, 641 ) 642 elif isinstance(df, dict): 643 df = pd.DataFrame.from_dict(df, npartitions=npartitions) 644 elif 'pandas.core.frame.DataFrame' in str(type(df)): 645 df = pd.from_pandas(df, npartitions=npartitions) 646 else: 647 raise Exception("Can only parse dictionaries or lists of dictionaries with Dask.") 648 pandas = attempt_import('pandas') 649 pdf = get_first_valid_dask_partition(df) 650 651 else: 652 df = pd.DataFrame(df).convert_dtypes(dtype_backend=dtype_backend) 653 pdf = df 654 655 ### skip parsing if DataFrame is empty 656 if len(pdf) == 0: 657 if debug: 658 dprint("df is empty. Returning original DataFrame without casting datetime columns...") 659 return df 660 661 ignore_cols = set( 662 (ignore_cols or []) + [ 663 col 664 for col, dtype in pdf.dtypes.items() 665 if 'datetime' in str(dtype) 666 ] 667 ) 668 cols_to_inspect = [ 669 col 670 for col in pdf.columns 671 if col not in ignore_cols 672 ] if not ignore_all else [] 673 674 if len(cols_to_inspect) == 0: 675 if debug: 676 dprint("All columns are ignored, skipping datetime detection...") 677 return df.infer_objects().fillna(pandas.NA) 678 679 ### apply regex to columns to determine which are ISO datetimes 680 iso_dt_regex = r'\d{4}-\d{2}-\d{2}.\d{2}\:\d{2}\:\d+' 681 dt_mask = pdf[cols_to_inspect].astype(str).apply( 682 lambda s: s.str.match(iso_dt_regex).all() 683 ) 684 685 ### list of datetime column names 686 datetime_cols = [col for col in pdf[cols_to_inspect].loc[:, dt_mask]] 687 if not datetime_cols: 688 if debug: 689 dprint("No columns detected as datetimes, returning...") 690 return df.infer_objects().fillna(pandas.NA) 691 692 if debug: 693 dprint("Converting columns to datetimes: " + str(datetime_cols)) 694 695 def _parse_to_datetime(x): 696 return to_datetime(x, precision_unit=precision_unit, coerce_utc=coerce_utc) 697 698 try: 699 if not using_dask: 700 df[datetime_cols] = df[datetime_cols].apply(_parse_to_datetime) 701 else: 702 df[datetime_cols] = df[datetime_cols].apply( 703 _parse_to_datetime, 704 utc=True, 705 axis=1, 706 meta={ 707 col: MRSM_PD_DTYPES['datetime'] 708 for col in datetime_cols 709 } 710 ) 711 except Exception: 712 warn( 713 f"Unable to apply `to_datetime()` to {items_str(datetime_cols)}:\n" 714 + f"{traceback.format_exc()}" 715 ) 716 717 if strip_timezone: 718 for dt in datetime_cols: 719 try: 720 df[dt] = df[dt].dt.tz_localize(None) 721 except Exception: 722 warn( 723 f"Unable to convert column '{dt}' to naive datetime:\n" 724 + f"{traceback.format_exc()}" 725 ) 726 727 return df.fillna(pandas.NA)
Parse a pandas DataFrame for datetime columns and cast as datetimes.
Parameters
- df (pd.DataFrame): The pandas DataFrame to parse.
- ignore_cols (Optional[Iterable[str]], default None): If provided, do not attempt to coerce these columns as datetimes.
- strip_timezone (bool, default False):
If
True, remove the UTCtzinfoproperty. - chunksize (Optional[int], default None):
If the pandas implementation is
'dask', use this chunksize for the distributed dataframe. - dtype_backend (str, default 'numpy_nullable'):
If
dfis not a DataFrame and new one needs to be constructed, use this as the datatypes backend. Accepted values are 'numpy_nullable' and 'pyarrow'. - ignore_all (bool, default False):
If
True, do not attempt to cast any columns to datetimes. - precision_unit (Optional[str], default None): If provided, enforce the given precision on the coerced datetime columns.
- coerce_utc (bool, default True):
Coerce the datetime columns to UTC (see
meerschaum.utils.dtypes.to_datetime()). - debug (bool, default False): Verbosity toggle.
Returns
- A new pandas DataFrame with the determined datetime columns
- (usually ISO strings) cast as datetimes.
Examples
>>> import pandas as pd
>>> df = pd.DataFrame({'a': ['2022-01-01 00:00:00']})
>>> df.dtypes
a object
dtype: object
>>> df2 = parse_df_datetimes(df)
>>> df2.dtypes
a datetime64[us, UTC]
dtype: object
730def get_unhashable_cols(df: 'pd.DataFrame') -> List[str]: 731 """ 732 Get the columns which contain unhashable objects from a Pandas DataFrame. 733 734 Parameters 735 ---------- 736 df: pd.DataFrame 737 The DataFrame which may contain unhashable objects. 738 739 Returns 740 ------- 741 A list of columns. 742 """ 743 if df is None: 744 return [] 745 if len(df) == 0: 746 return [] 747 748 is_dask = 'dask' in df.__module__ 749 if is_dask: 750 from meerschaum.utils.packages import attempt_import 751 pandas = attempt_import('pandas') 752 df = pandas.DataFrame(get_first_valid_dask_partition(df)) 753 return [ 754 col for col, val in df.iloc[0].items() 755 if not isinstance(val, Hashable) 756 ]
Get the columns which contain unhashable objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain unhashable objects.
Returns
- A list of columns.
759def get_json_cols(df: 'pd.DataFrame') -> List[str]: 760 """ 761 Get the columns which contain unhashable objects from a Pandas DataFrame. 762 763 Parameters 764 ---------- 765 df: pd.DataFrame 766 The DataFrame which may contain unhashable objects. 767 768 Returns 769 ------- 770 A list of columns to be encoded as JSON. 771 """ 772 _rust = _core.get_json_cols 773 return _rust(df)
Get the columns which contain unhashable objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain unhashable objects.
Returns
- A list of columns to be encoded as JSON.
776def get_numeric_cols(df: 'pd.DataFrame') -> List[str]: 777 """ 778 Get the columns which contain `decimal.Decimal` objects from a Pandas DataFrame. 779 780 Parameters 781 ---------- 782 df: pd.DataFrame 783 The DataFrame which may contain decimal objects. 784 785 Returns 786 ------- 787 A list of columns to treat as numerics. 788 """ 789 _rust = _core.get_numeric_cols 790 return _rust(df)
Get the columns which contain decimal.Decimal objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain decimal objects.
Returns
- A list of columns to treat as numerics.
793def get_bool_cols(df: 'pd.DataFrame') -> List[str]: 794 """ 795 Get the columns which contain `bool` objects from a Pandas DataFrame. 796 797 Parameters 798 ---------- 799 df: pd.DataFrame 800 The DataFrame which may contain bools. 801 802 Returns 803 ------- 804 A list of columns to treat as bools. 805 """ 806 if df is None: 807 return [] 808 809 is_dask = 'dask' in df.__module__ 810 if is_dask: 811 df = get_first_valid_dask_partition(df) 812 813 if len(df) == 0: 814 return [] 815 816 from meerschaum.utils.dtypes import are_dtypes_equal 817 818 return [ 819 col 820 for col, typ in df.dtypes.items() 821 if are_dtypes_equal(str(typ), 'bool') 822 ]
Get the columns which contain bool objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain bools.
Returns
- A list of columns to treat as bools.
825def get_uuid_cols(df: 'pd.DataFrame') -> List[str]: 826 """ 827 Get the columns which contain `uuid.UUID` objects from a Pandas DataFrame. 828 829 Parameters 830 ---------- 831 df: pd.DataFrame 832 The DataFrame which may contain UUID objects. 833 834 Returns 835 ------- 836 A list of columns to treat as UUIDs. 837 """ 838 _rust = _core.get_uuid_cols 839 return _rust(df)
Get the columns which contain uuid.UUID objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain UUID objects.
Returns
- A list of columns to treat as UUIDs.
842def get_datetime_cols( 843 df: 'pd.DataFrame', 844 timezone_aware: bool = True, 845 timezone_naive: bool = True, 846 with_tz_precision: bool = False, 847) -> Union[List[str], Dict[str, Tuple[Union[str, None], str]]]: 848 """ 849 Get the columns which contain `datetime` or `Timestamp` objects from a Pandas DataFrame. 850 851 Parameters 852 ---------- 853 df: pd.DataFrame 854 The DataFrame which may contain `datetime` or `Timestamp` objects. 855 856 timezone_aware: bool, default True 857 If `True`, include timezone-aware datetime columns. 858 859 timezone_naive: bool, default True 860 If `True`, include timezone-naive datetime columns. 861 862 with_tz_precision: bool, default False 863 If `True`, return a dictionary mapping column names to tuples in the form 864 `(timezone, precision)`. 865 866 Returns 867 ------- 868 A list of columns to treat as datetimes, or a dictionary of columns to tz+precision tuples 869 (if `with_tz_precision` is `True`). 870 """ 871 _rust = _core.get_datetime_cols 872 return _rust( 873 df, timezone_aware=timezone_aware, timezone_naive=timezone_naive, 874 with_tz_precision=with_tz_precision, 875 )
Get the columns which contain datetime or Timestamp objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame):
The DataFrame which may contain
datetimeorTimestampobjects. - timezone_aware (bool, default True):
If
True, include timezone-aware datetime columns. - timezone_naive (bool, default True):
If
True, include timezone-naive datetime columns. - with_tz_precision (bool, default False):
If
True, return a dictionary mapping column names to tuples in the form(timezone, precision).
Returns
- A list of columns to treat as datetimes, or a dictionary of columns to tz+precision tuples
- (if
with_tz_precisionisTrue).
878def get_datetime_cols_types(df: 'pd.DataFrame') -> Dict[str, str]: 879 """ 880 Return a dictionary mapping datetime columns to specific types strings. 881 882 Parameters 883 ---------- 884 df: pd.DataFrame 885 The DataFrame which may contain datetime columns. 886 887 Returns 888 ------- 889 A dictionary mapping the datetime columns' names to dtype strings 890 (containing timezone and precision metadata). 891 892 Examples 893 -------- 894 >>> from datetime import datetime, timezone 895 >>> import pandas as pd 896 >>> df = pd.DataFrame({'dt_tz_aware': [datetime(2025, 1, 1, tzinfo=timezone.utc)]}) 897 >>> get_datetime_cols_types(df) 898 {'dt_tz_aware': 'datetime64[us, UTC]'} 899 >>> df = pd.DataFrame({'distant_dt': [datetime(1, 1, 1)]}) 900 >>> get_datetime_cols_types(df) 901 {'distant_dt': 'datetime64[us]'} 902 >>> df = pd.DataFrame({'dt_second': datetime(2025, 1, 1)}) 903 >>> df['dt_second'] = df['dt_second'].astype('datetime64[s]') 904 >>> get_datetime_cols_types(df) 905 {'dt_second': 'datetime64[s]'} 906 """ 907 from meerschaum.utils.dtypes import MRSM_PRECISION_UNITS_ABBREVIATIONS 908 dt_cols_tuples = get_datetime_cols(df, with_tz_precision=True) 909 if not dt_cols_tuples: 910 return {} 911 912 return { 913 col: ( 914 f"datetime64[{MRSM_PRECISION_UNITS_ABBREVIATIONS[precision]}]" 915 if tz is None 916 else f"datetime64[{MRSM_PRECISION_UNITS_ABBREVIATIONS[precision]}, {tz}]" 917 ) 918 for col, (tz, precision) in dt_cols_tuples.items() 919 }
Return a dictionary mapping datetime columns to specific types strings.
Parameters
- df (pd.DataFrame): The DataFrame which may contain datetime columns.
Returns
- A dictionary mapping the datetime columns' names to dtype strings
- (containing timezone and precision metadata).
Examples
>>> from datetime import datetime, timezone
>>> import pandas as pd
>>> df = pd.DataFrame({'dt_tz_aware': [datetime(2025, 1, 1, tzinfo=timezone.utc)]})
>>> get_datetime_cols_types(df)
{'dt_tz_aware': 'datetime64[us, UTC]'}
>>> df = pd.DataFrame({'distant_dt': [datetime(1, 1, 1)]})
>>> get_datetime_cols_types(df)
{'distant_dt': 'datetime64[us]'}
>>> df = pd.DataFrame({'dt_second': datetime(2025, 1, 1)})
>>> df['dt_second'] = df['dt_second'].astype('datetime64[s]')
>>> get_datetime_cols_types(df)
{'dt_second': 'datetime64[s]'}
922def get_date_cols(df: 'pd.DataFrame') -> List[str]: 923 """ 924 Get the `date` columns from a Pandas DataFrame. 925 926 Parameters 927 ---------- 928 df: pd.DataFrame 929 The DataFrame which may contain dates. 930 931 Returns 932 ------- 933 A list of columns to treat as dates. 934 """ 935 from meerschaum.utils.dtypes import are_dtypes_equal 936 if df is None: 937 return [] 938 939 is_dask = 'dask' in df.__module__ 940 if is_dask: 941 df = get_first_valid_dask_partition(df) 942 943 known_date_cols = [ 944 col 945 for col, typ in df.dtypes.items() 946 if are_dtypes_equal(typ, 'date') 947 ] 948 949 if len(df) == 0: 950 return known_date_cols 951 952 cols_indices = { 953 col: df[col].first_valid_index() 954 for col in df.columns 955 if col not in known_date_cols 956 } 957 object_date_cols = [ 958 col 959 for col, ix in cols_indices.items() 960 if ( 961 ix is not None 962 and isinstance(df.loc[ix][col], date) 963 ) 964 ] 965 966 all_date_cols = set(known_date_cols + object_date_cols) 967 968 return [ 969 col 970 for col in df.columns 971 if col in all_date_cols 972 ]
Get the date columns from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain dates.
Returns
- A list of columns to treat as dates.
975def get_bytes_cols(df: 'pd.DataFrame') -> List[str]: 976 """ 977 Get the columns which contain bytes strings from a Pandas DataFrame. 978 979 Parameters 980 ---------- 981 df: pd.DataFrame 982 The DataFrame which may contain bytes strings. 983 984 Returns 985 ------- 986 A list of columns to treat as bytes. 987 """ 988 _rust = _core.get_bytes_cols 989 return _rust(df)
Get the columns which contain bytes strings from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain bytes strings.
Returns
- A list of columns to treat as bytes.
992def get_geometry_cols( 993 df: 'pd.DataFrame', 994 with_types_srids: bool = False, 995) -> Union[List[str], Dict[str, Any]]: 996 """ 997 Get the columns which contain shapely objects from a Pandas DataFrame. 998 999 Parameters 1000 ---------- 1001 df: pd.DataFrame 1002 The DataFrame which may contain bytes strings. 1003 1004 with_types_srids: bool, default False 1005 If `True`, return a dictionary mapping columns to geometry types and SRIDs. 1006 1007 Returns 1008 ------- 1009 A list of columns to treat as `geometry`. 1010 If `with_types_srids`, return a dictionary mapping columns to tuples in the form (type, SRID). 1011 """ 1012 _rust = _core.get_geometry_cols 1013 return _rust(df, with_types_srids=with_types_srids)
Get the columns which contain shapely objects from a Pandas DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame which may contain bytes strings.
- with_types_srids (bool, default False):
If
True, return a dictionary mapping columns to geometry types and SRIDs.
Returns
- A list of columns to treat as
geometry. - If
with_types_srids, return a dictionary mapping columns to tuples in the form (type, SRID).
1016def get_geometry_cols_types(df: 'pd.DataFrame') -> Dict[str, str]: 1017 """ 1018 Return a dtypes dictionary mapping columns to specific geometry types (type, srid). 1019 """ 1020 geometry_cols_types_srids = get_geometry_cols(df, with_types_srids=True) 1021 new_cols_types = {} 1022 for col, (geometry_type, srid) in geometry_cols_types_srids.items(): 1023 new_dtype = "geometry" 1024 modifier = "" 1025 if not srid and geometry_type.lower() == 'geometry': 1026 new_cols_types[col] = new_dtype 1027 continue 1028 1029 modifier = "[" 1030 if geometry_type.lower() != 'geometry': 1031 modifier += f"{geometry_type}" 1032 1033 if srid: 1034 if modifier != '[': 1035 modifier += ", " 1036 modifier += f"{srid}" 1037 modifier += "]" 1038 new_cols_types[col] = f"{new_dtype}{modifier}" 1039 return new_cols_types
Return a dtypes dictionary mapping columns to specific geometry types (type, srid).
1042def get_special_cols(df: 'pd.DataFrame') -> Dict[str, str]: 1043 """ 1044 Return a dtypes dictionary mapping special columns to their dtypes. 1045 """ 1046 return { 1047 **{col: 'json' for col in get_json_cols(df)}, 1048 **{col: 'uuid' for col in get_uuid_cols(df)}, 1049 **{col: 'bytes' for col in get_bytes_cols(df)}, 1050 **{col: 'bool' for col in get_bool_cols(df)}, 1051 **{col: 'numeric' for col in get_numeric_cols(df)}, 1052 **{col: 'date' for col in get_date_cols(df)}, 1053 **get_datetime_cols_types(df), 1054 **get_geometry_cols_types(df), 1055 }
Return a dtypes dictionary mapping special columns to their dtypes.
1058def enforce_dtypes( 1059 df: 'pd.DataFrame', 1060 dtypes: Dict[str, str], 1061 explicit_dtypes: Optional[Dict[str, str]] = None, 1062 safe_copy: bool = True, 1063 coerce_numeric: bool = False, 1064 coerce_timezone: bool = True, 1065 strip_timezone: bool = False, 1066 as_polars: Optional[bool] = None, 1067 debug: bool = False, 1068 as_pandas: Optional[bool] = None, 1069) -> 'pd.DataFrame': 1070 """ 1071 Enforce the `dtypes` dictionary on a DataFrame. 1072 1073 Parameters 1074 ---------- 1075 df: pd.DataFrame 1076 The DataFrame on which to enforce dtypes. 1077 1078 dtypes: Dict[str, str] 1079 The data types to attempt to enforce on the DataFrame. 1080 1081 explicit_dtypes: Optional[Dict[str, str]], default None 1082 If provided, automatic dtype coersion will respect explicitly configured 1083 dtypes (`int`, `float`, `numeric`). 1084 1085 safe_copy: bool, default True 1086 If `True`, create a copy before comparing and modifying the dataframes. 1087 Setting to `False` may mutate the DataFrames. 1088 See `meerschaum.utils.dataframe.filter_unseen_df`. 1089 1090 coerce_numeric: bool, default False 1091 If `True`, convert float and int collisions to numeric. 1092 1093 coerce_timezone: bool, default True 1094 If `True`, convert datetimes to UTC. 1095 1096 strip_timezone: bool, default False 1097 If `coerce_timezone` and `strip_timezone` are `True`, 1098 remove timezone information from datetimes. 1099 1100 as_polars: Optional[bool], default None 1101 `True` returns a Polars DataFrame, `False` a pandas one. By default the result is the 1102 library `df` came in (a Polars frame stays Polars, a pandas frame pandas). 1103 1104 debug: bool, default False 1105 Verbosity toggle. 1106 1107 as_pandas: Optional[bool], default None 1108 `True` returns a pandas DataFrame, `False` a Polars one (see `as_polars`). 1109 1110 Returns 1111 ------- 1112 The DataFrame with the types enforced, of the library `df` came in (or the one asked for). 1113 """ 1114 if df is None: 1115 return df 1116 library = output_library_for(df, as_polars=as_polars, as_pandas=as_pandas) 1117 _rust = _core.enforce_dtypes 1118 return _rust( 1119 df, dtypes=dtypes, explicit_dtypes=explicit_dtypes, safe_copy=safe_copy, 1120 coerce_numeric=coerce_numeric, coerce_timezone=coerce_timezone, 1121 strip_timezone=strip_timezone, as_polars=(library == 'polars'), debug=debug, 1122 )
Enforce the dtypes dictionary on a DataFrame.
Parameters
- df (pd.DataFrame): The DataFrame on which to enforce dtypes.
- dtypes (Dict[str, str]): The data types to attempt to enforce on the DataFrame.
- explicit_dtypes (Optional[Dict[str, str]], default None):
If provided, automatic dtype coersion will respect explicitly configured
dtypes (
int,float,numeric). - safe_copy (bool, default True):
If
True, create a copy before comparing and modifying the dataframes. Setting toFalsemay mutate the DataFrames. Seemeerschaum.utils.dataframe.filter_unseen_df. - coerce_numeric (bool, default False):
If
True, convert float and int collisions to numeric. - coerce_timezone (bool, default True):
If
True, convert datetimes to UTC. - strip_timezone (bool, default False):
If
coerce_timezoneandstrip_timezoneareTrue, remove timezone information from datetimes. - as_polars (Optional[bool], default None):
Truereturns a Polars DataFrame,Falsea pandas one. By default the result is the librarydfcame in (a Polars frame stays Polars, a pandas frame pandas). - debug (bool, default False): Verbosity toggle.
- as_pandas (Optional[bool], default None):
Truereturns a pandas DataFrame,Falsea Polars one (seeas_polars).
Returns
- The DataFrame with the types enforced, of the library
dfcame in (or the one asked for).
1125def get_datetime_bound_from_df( 1126 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]], 1127 datetime_column: str, 1128 minimum: bool = True, 1129) -> Union[int, datetime, None]: 1130 """ 1131 Return the minimum or maximum datetime (or integer) from a DataFrame. 1132 1133 Parameters 1134 ---------- 1135 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]] 1136 The DataFrame, list, or dict which contains the range axis. 1137 1138 datetime_column: str 1139 The name of the datetime (or int) column. 1140 1141 minimum: bool 1142 Whether to return the minimum (default) or maximum value. 1143 1144 Returns 1145 ------- 1146 The minimum or maximum datetime value in the dataframe, or `None`. 1147 """ 1148 from meerschaum.utils.dtypes import to_datetime, value_is_null 1149 1150 if df is None: 1151 return None 1152 if not datetime_column: 1153 return None 1154 1155 def compare(a, b): 1156 if a is None: 1157 return b 1158 if b is None: 1159 return a 1160 if minimum: 1161 return a if a < b else b 1162 return a if a > b else b 1163 1164 if isinstance(df, list): 1165 if len(df) == 0: 1166 return None 1167 best_yet = df[0].get(datetime_column, None) 1168 for doc in df: 1169 val = doc.get(datetime_column, None) 1170 best_yet = compare(best_yet, val) 1171 return best_yet 1172 1173 if isinstance(df, dict): 1174 if datetime_column not in df: 1175 return None 1176 best_yet = df[datetime_column][0] 1177 for val in df[datetime_column]: 1178 best_yet = compare(best_yet, val) 1179 return best_yet 1180 1181 if 'DataFrame' in str(type(df)): 1182 from meerschaum.utils.dtypes import are_dtypes_equal 1183 pandas = mrsm.attempt_import('pandas') 1184 is_dask = 'dask' in df.__module__ 1185 1186 if datetime_column not in df.columns: 1187 return None 1188 1189 try: 1190 dt_val = ( 1191 df[datetime_column].min(skipna=True) 1192 if minimum 1193 else df[datetime_column].max(skipna=True) 1194 ) 1195 except Exception: 1196 dt_val = pandas.NA 1197 if is_dask and dt_val is not None and dt_val is not pandas.NA: 1198 dt_val = dt_val.compute() 1199 1200 return ( 1201 to_datetime(dt_val, as_pydatetime=True) 1202 if are_dtypes_equal(str(type(dt_val)), 'datetime') 1203 else (dt_val if not value_is_null(dt_val) else None) 1204 ) 1205 1206 return None
Return the minimum or maximum datetime (or integer) from a DataFrame.
Parameters
- df (Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]]): The DataFrame, list, or dict which contains the range axis.
- datetime_column (str): The name of the datetime (or int) column.
- minimum (bool): Whether to return the minimum (default) or maximum value.
Returns
- The minimum or maximum datetime value in the dataframe, or
None.
1209def get_unique_index_values( 1210 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]], 1211 indices: List[str], 1212) -> Dict[str, List[Any]]: 1213 """ 1214 Return a dictionary of the unique index values in a DataFrame. 1215 1216 Parameters 1217 ---------- 1218 df: Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]] 1219 The dataframe (or list or dict) which contains index values. 1220 1221 indices: List[str] 1222 The list of index columns. 1223 1224 Returns 1225 ------- 1226 A dictionary mapping indices to unique values. 1227 """ 1228 if df is None: 1229 return {} 1230 if 'dataframe' in str(type(df)).lower(): 1231 pandas = mrsm.attempt_import('pandas') 1232 return { 1233 col: list({ 1234 (val if val is not pandas.NA else None) 1235 for val in df[col].unique() 1236 }) 1237 for col in indices 1238 if col in df.columns 1239 } 1240 1241 unique_indices = defaultdict(lambda: set()) 1242 if isinstance(df, list): 1243 for doc in df: 1244 for index in indices: 1245 if index in doc: 1246 unique_indices[index].add(doc[index]) 1247 1248 elif isinstance(df, dict): 1249 for index in indices: 1250 if index in df: 1251 unique_indices[index] = unique_indices[index].union(set(df[index])) 1252 1253 return {key: list(val) for key, val in unique_indices.items()}
Return a dictionary of the unique index values in a DataFrame.
Parameters
- df (Union['pd.DataFrame', Dict[str, List[Any]], List[Dict[str, Any]]]): The dataframe (or list or dict) which contains index values.
- indices (List[str]): The list of index columns.
Returns
- A dictionary mapping indices to unique values.
1256def df_is_chunk_generator(df: Any) -> bool: 1257 """ 1258 Determine whether to treat `df` as a chunk generator. 1259 1260 Note this should only be used in a context where generators are expected, 1261 as it will return `True` for any iterable. 1262 1263 Parameters 1264 ---------- 1265 The DataFrame or chunk generator to evaluate. 1266 1267 Returns 1268 ------- 1269 A `bool` indicating whether to treat `df` as a generator. 1270 """ 1271 return ( 1272 not isinstance(df, (dict, list, str)) 1273 and 'DataFrame' not in str(type(df)) 1274 and isinstance(df, (Generator, Iterable, Iterator)) 1275 )
Determine whether to treat df as a chunk generator.
Note this should only be used in a context where generators are expected,
as it will return True for any iterable.
Parameters
- The DataFrame or chunk generator to evaluate.
Returns
- A
boolindicating whether to treatdfas a generator.
1278def chunksize_to_npartitions(chunksize: Optional[int]) -> int: 1279 """ 1280 Return the Dask `npartitions` value for a given `chunksize`. 1281 """ 1282 if chunksize == -1: 1283 from meerschaum.config import get_config 1284 chunksize = get_config('system', 'connectors', 'sql', 'chunksize') 1285 if chunksize is None: 1286 return 1 1287 return -1 * chunksize
Return the Dask npartitions value for a given chunksize.
1290def df_from_literal( 1291 pipe: Optional[mrsm.Pipe] = None, 1292 literal: Optional[str] = None, 1293 debug: bool = False 1294) -> 'pd.DataFrame': 1295 """ 1296 Construct a dataframe from a literal value, using the pipe's datetime and value column names. 1297 1298 Parameters 1299 ---------- 1300 pipe: Optional['meerschaum.Pipe'], default None 1301 The pipe which will consume the literal value. 1302 1303 Returns 1304 ------- 1305 A 1-row pandas DataFrame from with the current UTC timestamp as the datetime columns 1306 and the literal as the value. 1307 """ 1308 from meerschaum.utils.packages import import_pandas 1309 from meerschaum.utils.warnings import error, warn 1310 from meerschaum.utils.debug import dprint 1311 from meerschaum.utils.dtypes import get_current_timestamp 1312 1313 if pipe is None or literal is None: 1314 error("Please provide a Pipe and a literal value") 1315 1316 dt_col = pipe.columns.get( 1317 'datetime', 1318 mrsm.get_config('pipes', 'autotime', 'column_name_if_datetime_missing') 1319 ) 1320 val_col = pipe.get_val_column(debug=debug) 1321 1322 val = literal 1323 if isinstance(literal, str): 1324 if debug: 1325 dprint(f"Received literal string: '{literal}'") 1326 import ast 1327 try: 1328 val = ast.literal_eval(literal) 1329 except Exception: 1330 warn( 1331 "Failed to parse value from string:\n" + f"{literal}" + 1332 "\n\nWill cast as a string instead."\ 1333 ) 1334 val = literal 1335 1336 now = get_current_timestamp(pipe.precision) 1337 pd = import_pandas() 1338 return pd.DataFrame({dt_col: [now], val_col: [val]})
Construct a dataframe from a literal value, using the pipe's datetime and value column names.
Parameters
- pipe (Optional['meerschaum.Pipe'], default None): The pipe which will consume the literal value.
Returns
- A 1-row pandas DataFrame from with the current UTC timestamp as the datetime columns
- and the literal as the value.
1341def get_first_valid_dask_partition(ddf: 'dask.dataframe.DataFrame') -> Union['pd.DataFrame', None]: 1342 """ 1343 Return the first valid Dask DataFrame partition (if possible). 1344 """ 1345 pdf = None 1346 for partition in ddf.partitions: 1347 try: 1348 pdf = partition.compute() 1349 except Exception: 1350 continue 1351 if len(pdf) > 0: 1352 return pdf 1353 _ = mrsm.attempt_import('partd', lazy=False) 1354 return ddf.compute()
Return the first valid Dask DataFrame partition (if possible).
1357def query_df( 1358 df: 'pd.DataFrame', 1359 params: Optional[Dict[str, Any]] = None, 1360 begin: Union[datetime, int, None] = None, 1361 end: Union[datetime, int, None] = None, 1362 datetime_column: Optional[str] = None, 1363 select_columns: Optional[List[str]] = None, 1364 omit_columns: Optional[List[str]] = None, 1365 inplace: bool = False, 1366 reset_index: bool = False, 1367 coerce_types: bool = False, 1368 debug: bool = False, 1369) -> 'pd.DataFrame': 1370 """ 1371 Query the dataframe with the params dictionary. 1372 1373 Parameters 1374 ---------- 1375 df: pd.DataFrame 1376 The DataFrame to query against. 1377 1378 params: Optional[Dict[str, Any]], default None 1379 The parameters dictionary to use for the query. 1380 1381 begin: Union[datetime, int, None], default None 1382 If `begin` and `datetime_column` are provided, only return rows with a timestamp 1383 greater than or equal to this value. 1384 1385 end: Union[datetime, int, None], default None 1386 If `begin` and `datetime_column` are provided, only return rows with a timestamp 1387 less than this value. 1388 1389 datetime_column: Optional[str], default None 1390 A `datetime_column` must be provided to use `begin` and `end`. 1391 1392 select_columns: Optional[List[str]], default None 1393 If provided, only return these columns. 1394 1395 omit_columns: Optional[List[str]], default None 1396 If provided, do not include these columns in the result. 1397 1398 inplace: bool, default False 1399 If `True`, modify the DataFrame inplace rather than creating a new DataFrame. 1400 1401 reset_index: bool, default False 1402 If `True`, reset the index in the resulting DataFrame. 1403 1404 coerce_types: bool, default False 1405 If `True`, cast the dataframe and parameters as strings before querying. 1406 1407 Returns 1408 ------- 1409 A Pandas DataFrame query result. 1410 """ 1411 _rust = _core.query_df 1412 return _rust( 1413 df, params=params, begin=begin, end=end, datetime_column=datetime_column, 1414 select_columns=select_columns, omit_columns=omit_columns, inplace=inplace, 1415 reset_index=reset_index, coerce_types=coerce_types, debug=debug, 1416 )
Query the dataframe with the params dictionary.
Parameters
- df (pd.DataFrame): The DataFrame to query against.
- params (Optional[Dict[str, Any]], default None): The parameters dictionary to use for the query.
- begin (Union[datetime, int, None], default None):
If
beginanddatetime_columnare provided, only return rows with a timestamp greater than or equal to this value. - end (Union[datetime, int, None], default None):
If
beginanddatetime_columnare provided, only return rows with a timestamp less than this value. - datetime_column (Optional[str], default None):
A
datetime_columnmust be provided to usebeginandend. - select_columns (Optional[List[str]], default None): If provided, only return these columns.
- omit_columns (Optional[List[str]], default None): If provided, do not include these columns in the result.
- inplace (bool, default False):
If
True, modify the DataFrame inplace rather than creating a new DataFrame. - reset_index (bool, default False):
If
True, reset the index in the resulting DataFrame. - coerce_types (bool, default False):
If
True, cast the dataframe and parameters as strings before querying.
Returns
- A Pandas DataFrame query result.
1419def to_json( 1420 df: 'pd.DataFrame', 1421 safe_copy: bool = True, 1422 orient: str = 'records', 1423 date_format: str = 'iso', 1424 date_unit: str = 'us', 1425 double_precision: int = 15, 1426 geometry_format: str = 'geojson', 1427 **kwargs: Any 1428) -> str: 1429 """ 1430 Serialize the given dataframe as a JSON string. 1431 1432 Parameters 1433 ---------- 1434 df: pd.DataFrame 1435 The DataFrame to be serialized. 1436 1437 safe_copy: bool, default True 1438 If `False`, modify the DataFrame inplace. 1439 1440 date_format: str, default 'iso' 1441 The default format for timestamps. 1442 1443 date_unit: str, default 'us' 1444 The precision of the timestamps. 1445 1446 double_precision: int, default 15 1447 The number of decimal places to use when encoding floating point values (maximum 15). 1448 1449 geometry_format: str, default 'geojson' 1450 The serialization format for geometry data. 1451 Accepted values are `geojson`, `wkb_hex`, and `wkt`. 1452 1453 Returns 1454 ------- 1455 A JSON string. 1456 """ 1457 import warnings 1458 from meerschaum.utils.packages import import_pandas 1459 from meerschaum.utils.dtypes import ( 1460 serialize_bytes, 1461 serialize_decimal, 1462 serialize_geometry, 1463 serialize_date, 1464 ) 1465 pd = import_pandas() 1466 uuid_cols = get_uuid_cols(df) 1467 bytes_cols = get_bytes_cols(df) 1468 numeric_cols = get_numeric_cols(df) 1469 date_cols = get_date_cols(df) 1470 geometry_cols = get_geometry_cols(df) 1471 geometry_cols_srids = { 1472 col: int((getattr(df[col].crs, 'srs', '') or '').split(':', maxsplit=1)[-1] or '0') 1473 for col in geometry_cols 1474 } if 'geodataframe' in str(type(df)).lower() else {} 1475 if safe_copy and bool(uuid_cols or bytes_cols or geometry_cols or numeric_cols): 1476 df = df.copy() 1477 if 'geodataframe' in str(type(df)).lower(): 1478 geometry_data = { 1479 col: df[col] 1480 for col in geometry_cols 1481 } 1482 df = pd.DataFrame({ 1483 col: df[col] 1484 for col in df.columns 1485 if col not in geometry_cols 1486 }) 1487 for col in geometry_cols: 1488 df[col] = pd.Series(ob for ob in geometry_data[col]) 1489 for col in uuid_cols: 1490 df[col] = df[col].astype(str) 1491 for col in bytes_cols: 1492 df[col] = df[col].apply(serialize_bytes) 1493 for col in numeric_cols: 1494 df[col] = df[col].apply(serialize_decimal) 1495 for col in date_cols: 1496 df[col] = df[col].apply(serialize_date) 1497 with warnings.catch_warnings(): 1498 warnings.simplefilter("ignore") 1499 for col in geometry_cols: 1500 srid = geometry_cols_srids.get(col, None) or None 1501 df[col] = pd.Series( 1502 serialize_geometry(val, geometry_format=geometry_format, srid=srid) 1503 for val in df[col] 1504 ) 1505 return df.infer_objects().fillna(pd.NA).to_json( 1506 date_format=date_format, 1507 date_unit=date_unit, 1508 double_precision=double_precision, 1509 orient=orient, 1510 **kwargs 1511 )
Serialize the given dataframe as a JSON string.
Parameters
- df (pd.DataFrame): The DataFrame to be serialized.
- safe_copy (bool, default True):
If
False, modify the DataFrame inplace. - date_format (str, default 'iso'): The default format for timestamps.
- date_unit (str, default 'us'): The precision of the timestamps.
- double_precision (int, default 15): The number of decimal places to use when encoding floating point values (maximum 15).
- geometry_format (str, default 'geojson'):
The serialization format for geometry data.
Accepted values are
geojson,wkb_hex, andwkt.
Returns
- A JSON string.
1514def to_simple_lines(df: 'pd.DataFrame') -> str: 1515 """ 1516 Serialize a Pandas Dataframe as lines of simple dictionaries. 1517 1518 Parameters 1519 ---------- 1520 df: pd.DataFrame 1521 The dataframe to serialize into simple lines text. 1522 1523 Returns 1524 ------- 1525 A string of simple line dictionaries joined by newlines. 1526 """ 1527 from meerschaum.utils.misc import to_simple_dict 1528 if df is None or len(df) == 0: 1529 return '' 1530 1531 docs = df.to_dict(orient='records') 1532 return '\n'.join(to_simple_dict(doc) for doc in docs)
Serialize a Pandas Dataframe as lines of simple dictionaries.
Parameters
- df (pd.DataFrame): The dataframe to serialize into simple lines text.
Returns
- A string of simple line dictionaries joined by newlines.
1535def parse_simple_lines(data: str) -> 'pd.DataFrame': 1536 """ 1537 Parse simple lines text into a DataFrame. 1538 1539 Parameters 1540 ---------- 1541 data: str 1542 The simple lines text to parse into a DataFrame. 1543 1544 Returns 1545 ------- 1546 A dataframe containing the rows serialized in `data`. 1547 """ 1548 from meerschaum.utils.misc import string_to_dict 1549 from meerschaum.utils.packages import import_pandas 1550 pd = import_pandas() 1551 lines = data.splitlines() 1552 try: 1553 docs = [string_to_dict(line) for line in lines] 1554 df = pd.DataFrame(docs) 1555 except Exception: 1556 df = None 1557 1558 if df is None: 1559 raise ValueError("Cannot parse simple lines into a dataframe.") 1560 1561 return df
Parse simple lines text into a DataFrame.
Parameters
- data (str): The simple lines text to parse into a DataFrame.
Returns
- A dataframe containing the rows serialized in
data.