Source code for hdhelpers.metadata.helpers

  1"""Functions accessing the metadata of SERIES, SINGLETSFRAMEs and MULTITSFRAMEs
  2
  3The functions are thin wrappers over the methods of the models of the metadata conventions,
  4see read_metadata. The methods resolve the fallbacks of the conventions, e.g. a missing display
  5name falls back to the name, see hdhelpers.metadata._reading.
  6
  7The metadata is the model attached to the object (see attach_metadata), whatever data type it
  8belongs to, e.g. the metadata of a SINGLETSFRAME on a Series taken from it. Otherwise the
  9metadata in attrs is parsed as the data type of the function. Metadata which does not follow
 10the conventions gives no information: None or empty defaultdicts. For a SERIES or SINGLETSFRAME,
 11the single metric is the one named by dataset_metadata.single_metric, otherwise the only one.
 12
 13Fields of the functions taking one can be dotted paths, e.g. "inherited.site".
 14"""
 15
 16import datetime
 17from collections import defaultdict
 18from collections.abc import Callable
 19from typing import Any
 20
 21import pandas as pd
 22
 23from hdhelpers.metadata._reading import (
 24    DISPLAY_NAME_FIELDS,
 25    SHORT_DISPLAY_NAME_FIELDS,
 26    Recording,
 27    dataset_default_timezone,
 28    dataset_recording,
 29)
 30from hdhelpers.metadata.attached import META_KEY, AnyMetadata, infer_data_type, read_metadata
 31from hdhelpers.metadata.conventions import TimeseriesDataType, _TimeseriesMetadataBase
 32
 33_DEFAULT_AGGREGATION_METHOD = "MEAN"
 34
 35
 36def check_dataframe(data: pd.DataFrame) -> None:
 37    if not isinstance(data, pd.DataFrame):
 38        raise TypeError("Input is not a pd.DataFrame")
 39
 40
 41def check_series(series: pd.Series) -> None:
 42    if not isinstance(series, pd.Series):
 43        raise TypeError("Input is not a pd.Series.")
 44
 45
 46def check_series_or_dataframe(data: pd.Series | pd.DataFrame) -> None:
 47    if not isinstance(data, pd.Series) and not isinstance(data, pd.DataFrame):
 48        raise TypeError("Input is not a pd.Series or pd.DataFrame.")
 49
 50
 51def _metadata(data: pd.Series | pd.DataFrame, data_type: TimeseriesDataType) -> AnyMetadata | None:
 52    """The attached metadata of any data type, otherwise the attrs parsed as data_type"""
 53    attached = data.attrs.get(META_KEY)
 54    if isinstance(attached, _TimeseriesMetadataBase):
 55        return attached  # type: ignore[return-value]
 56    return read_metadata(data, data_type)
 57
 58
 59def _single_metric(meta: AnyMetadata | None) -> str | None:
 60    """The single metric: dataset_metadata.single_metric if described, otherwise the only one"""
 61    if meta is None:
 62        return None
 63    metric_ids = meta.metric_ids()
 64    single_metric = meta.dataset_metadata.single_metric
 65    if single_metric is not None:
 66        return single_metric if single_metric in metric_ids else None
 67    return metric_ids[0] if len(metric_ids) == 1 else None
 68
 69
 70def _by_metric_by_column(
 71    data: pd.DataFrame, fields: str | tuple[str, ...], include_inherited: bool
 72) -> defaultdict[str, defaultdict[str, Any]]:
 73    check_dataframe(data)
 74    meta = _metadata(data, "MULTITSFRAME")
 75    infos = (
 76        {}
 77        if meta is None
 78        else meta.value_dimension_infos(fields, include_inherited=include_inherited)
 79    )
 80    return defaultdict(
 81        lambda: defaultdict(lambda: None),
 82        {metric: defaultdict(lambda: None, info) for metric, info in infos.items()},
 83    )
 84
 85
 86def _single_metric_by_column(
 87    data: pd.DataFrame, fields: str | tuple[str, ...], include_inherited: bool
 88) -> defaultdict[str, Any]:
 89    check_dataframe(data)
 90    meta = _metadata(data, "SINGLETSFRAME")
 91    metric = _single_metric(meta)
 92    if meta is None or metric is None:
 93        return defaultdict(lambda: None)
 94    return defaultdict(
 95        lambda: None,
 96        meta.value_dimension_infos(fields, include_inherited=include_inherited)[metric],
 97    )
 98
 99
100def _series_value(series: pd.Series, fields: str | tuple[str, ...], include_inherited: bool) -> Any:
101    check_series(series)
102    meta = _metadata(series, "SERIES")
103    metric = _single_metric(meta)
104    if meta is None or metric is None:
105        return None
106    return meta.value_dimension_info(fields, metric, "value", include_inherited=include_inherited)
107
108
109def _single_metric_or_dataset(
110    data: pd.Series | pd.DataFrame,
111    data_type: TimeseriesDataType,
112    of_metric: Callable[[AnyMetadata, str], Any],
113    of_dataset: Callable[[AnyMetadata], Any],
114    empty: Any,
115) -> Any:
116    """Information of the single metric, of the dataset metadata if the metric is not found"""
117    meta = _metadata(data, data_type)
118    if meta is None:
119        return empty
120    metric = _single_metric(meta)
121    return of_dataset(meta) if metric is None else of_metric(meta, metric)
122
123
124# value dimensions of MULTITSFRAMEs
125
126
[docs] 127def get_units( 128 multitsframe: pd.DataFrame, *, include_inherited: bool = False 129) -> defaultdict[str, defaultdict[str, str | None]]: 130 """Read units for each metric and value column. 131 132 Args: 133 multitsframe: DataFrame with per-metric metadata in attrs. 134 include_inherited: Use inherited fields last, only for the "value" column. 135 136 Returns: 137 defaultdict[str, defaultdict[str, str | None]]: 138 Units as result[metric_id][column]; unavailable units are None. 139 140 Code example: 141 142 .. doctest:: metadata.get_units 143 144 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 145 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 146 >>> units = md.get_units(multitsframe) 147 >>> units["plant_a.temperature"]["value"] 148 '°C' 149 >>> units["unknown metric"]["value"] is None 150 True 151 152 See :ref:`metadata-getter-values` for shared examples and return shapes. 153 """ # noqa: E501 154 return _by_metric_by_column(multitsframe, "unit", include_inherited)
155 156
[docs] 157def get_names( 158 multitsframe: pd.DataFrame, *, include_inherited: bool = False 159) -> defaultdict[str, defaultdict[str, str | None]]: 160 """Read metadata names for each metric and value column. 161 162 For the "value" column, try the metric's name after the column's own name. 163 164 Args: 165 multitsframe: DataFrame with per-metric metadata in attrs. 166 include_inherited: Use inherited fields last, only for the "value" column. 167 168 Returns: 169 defaultdict[str, defaultdict[str, str | None]]: 170 Names as result[metric_id][column]; unavailable names are None. 171 172 Code example: 173 174 .. doctest:: metadata.get_names 175 176 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 177 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 178 >>> md.get_names(multitsframe)["plant_a.pressure"]["value"] 179 'Plant A pressure' 180 181 See :ref:`metadata-getter-names` for shared examples and return shapes. 182 """ 183 return _by_metric_by_column(multitsframe, "name", include_inherited)
184 185
[docs] 186def get_display_names( 187 multitsframe: pd.DataFrame, *, include_inherited: bool = False 188) -> defaultdict[str, defaultdict[str, str | None]]: 189 """Read display labels for each metric and value column. 190 191 Use display_name, then name. 192 193 Args: 194 multitsframe: DataFrame with per-metric metadata in attrs. 195 include_inherited: Use inherited fields last, only for the "value" column. 196 197 Returns: 198 defaultdict[str, defaultdict[str, str | None]]: 199 Labels as result[metric_id][column]; unavailable labels are None. 200 201 See :ref:`metadata-getter-names` for shared examples and return shapes. 202 """ 203 return _by_metric_by_column(multitsframe, DISPLAY_NAME_FIELDS, include_inherited)
204 205
[docs] 206def get_short_display_names( 207 multitsframe: pd.DataFrame, *, include_inherited: bool = False 208) -> defaultdict[str, defaultdict[str, str | None]]: 209 """Read short display labels for each metric and value column. 210 211 Use short_display_name, then display_name, then name. 212 213 Args: 214 multitsframe: DataFrame with per-metric metadata in attrs. 215 include_inherited: Use inherited fields last, only for the "value" column. 216 217 Returns: 218 defaultdict[str, defaultdict[str, str | None]]: 219 Labels as result[metric_id][column]; unavailable labels are None. 220 221 See :ref:`metadata-getter-names` for shared examples and return shapes. 222 """ 223 return _by_metric_by_column(multitsframe, SHORT_DISPLAY_NAME_FIELDS, include_inherited)
224 225
[docs] 226def get_measurements( 227 multitsframe: pd.DataFrame, *, include_inherited: bool = False 228) -> defaultdict[str, defaultdict[str, str | None]]: 229 """Read physical quantities for each metric and value column. 230 231 For example, "temperature" is a measurement and "°C" is its unit. 232 233 Args: 234 multitsframe: DataFrame with per-metric metadata in attrs. 235 include_inherited: Use inherited fields last, only for the "value" column. 236 237 Returns: 238 defaultdict[str, defaultdict[str, str | None]]: 239 Quantities as result[metric_id][column]; unavailable quantities are None. 240 241 See :ref:`metadata-getter-values` for shared examples and return shapes. 242 """ 243 return _by_metric_by_column(multitsframe, "measurement", include_inherited)
244 245
[docs] 246def get_metric_info( 247 multitsframe: pd.DataFrame, metric_info: str, *, include_inherited: bool = True 248) -> defaultdict[str, Any]: 249 """Read a metadata field for each metric. 250 251 Search the metric, its "value" column, shared "value" metadata, then inherited fields 252 (if enabled). 253 254 Args: 255 multitsframe: DataFrame with per-metric metadata in attrs. 256 metric_info: Field name or dotted path, e.g. "sensor.model". 257 include_inherited: Use inherited fields as a last fallback. 258 259 Returns: 260 defaultdict[str, Any]: Field values by metric ID; unresolved fields are None. 261 262 Code example: 263 264 .. doctest:: metadata.get_metric_info 265 266 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 267 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 268 >>> md.get_metric_info(multitsframe, "recording_mode")["plant_a.temperature"] 269 'on_change' 270 271 See :ref:`metadata-getter-fields` for shared examples and return shapes. 272 """ 273 check_dataframe(multitsframe) 274 meta = _metadata(multitsframe, "MULTITSFRAME") 275 infos = ( 276 {} if meta is None else meta.metric_infos(metric_info, include_inherited=include_inherited) 277 ) 278 return defaultdict(lambda: None, infos)
279 280 281# the value dimension "value" of SERIES 282 283
[docs] 284def get_series_info( 285 series: pd.Series, value_dim_info: str, *, include_inherited: bool = True 286) -> Any: 287 """Read a metadata field for this Series. 288 289 Search the "value" column, the metric, shared "value" metadata, then inherited fields 290 (if enabled). 291 292 Args: 293 series: Series with metadata in attrs. 294 value_dim_info: Field name or dotted path, e.g. "sensor.model". 295 include_inherited: Use inherited fields as a last fallback. 296 297 Returns: 298 Any: Field value, or None when unavailable. 299 300 Code example: 301 302 .. doctest:: metadata.get_series_info 303 304 >>> from hdhelpers.metadata.conventions import SERIES_METADATA_EXAMPLE 305 >>> series.attrs = SERIES_METADATA_EXAMPLE 306 >>> md.get_series_info(series, "decimals") 307 2 308 309 See :ref:`metadata-getter-fields` for shared examples and return shapes. 310 """ 311 return _series_value(series, value_dim_info, include_inherited)
312 313
[docs] 314def get_series_unit(series: pd.Series, *, include_inherited: bool = False) -> str | None: 315 """Read this Series' unit from metadata. 316 317 Args: 318 series: Series with metadata in attrs. 319 include_inherited: Use inherited fields as a last fallback. 320 321 Returns: 322 str | None: Unit string; "" means dimensionless and None means unavailable. 323 324 Code example: 325 326 .. doctest:: metadata.get_series_unit 327 328 >>> from hdhelpers.metadata.conventions import SERIES_METADATA_EXAMPLE 329 >>> series.attrs = SERIES_METADATA_EXAMPLE 330 >>> md.get_series_unit(series) 331 'm' 332 333 See :ref:`metadata-getter-values` for shared examples and return shapes. 334 """ 335 return _series_value(series, "unit", include_inherited) # type: ignore[no-any-return]
336 337
[docs] 338def get_series_name(series: pd.Series, *, include_inherited: bool = False) -> str | None: 339 """Read this Series' name from metadata. 340 341 Try the "value" column's name, then the metric's name. 342 343 Args: 344 series: Series with metadata in attrs. 345 include_inherited: Use inherited fields as a last fallback. 346 347 Returns: 348 str | None: Resolved name, or None when unavailable. 349 350 Code example: 351 352 .. doctest:: metadata.get_series_name 353 354 >>> from hdhelpers.metadata.conventions import SERIES_METADATA_EXAMPLE 355 >>> series.attrs = SERIES_METADATA_EXAMPLE 356 >>> md.get_series_name(series) 357 'Water level north' 358 359 See :ref:`metadata-getter-names` for shared examples and return shapes. 360 """ 361 return _series_value(series, "name", include_inherited) # type: ignore[no-any-return]
362 363
[docs] 364def get_series_display_name(series: pd.Series, *, include_inherited: bool = False) -> str | None: 365 """Read the display label for this Series. 366 367 Use display_name, then name. 368 369 Args: 370 series: Series with metadata in attrs. 371 include_inherited: Use inherited fields as a last fallback. 372 373 Returns: 374 str | None: Resolved label, or None when unavailable. 375 376 See :ref:`metadata-getter-names` for shared examples and return shapes. 377 """ 378 return _series_value( # type: ignore[no-any-return] 379 series, DISPLAY_NAME_FIELDS, include_inherited 380 )
381 382
[docs] 383def get_series_short_display_name( 384 series: pd.Series, *, include_inherited: bool = False 385) -> str | None: 386 """Read the short display label for this Series. 387 388 Use short_display_name, then display_name, then name. 389 390 Args: 391 series: Series with metadata in attrs. 392 include_inherited: Use inherited fields as a last fallback. 393 394 Returns: 395 str | None: Resolved label, or None when unavailable. 396 397 See :ref:`metadata-getter-names` for shared examples and return shapes. 398 """ 399 return _series_value( # type: ignore[no-any-return] 400 series, SHORT_DISPLAY_NAME_FIELDS, include_inherited 401 )
402 403
[docs] 404def get_series_measurement(series: pd.Series, *, include_inherited: bool = False) -> str | None: 405 """Read this Series' physical quantity from metadata. 406 407 For example, "temperature" describes the quantity and "°C" describes its unit. 408 409 Args: 410 series: Series with metadata in attrs. 411 include_inherited: Use inherited fields as a last fallback. 412 413 Returns: 414 str | None: Quantity name, or None when unavailable. 415 416 See :ref:`metadata-getter-values` for shared examples and return shapes. 417 """ 418 return _series_value(series, "measurement", include_inherited) # type: ignore[no-any-return]
419 420 421# value dimensions of SINGLETSFRAMEs 422 423
[docs] 424def get_singlets_info( 425 singletsframe: pd.DataFrame, value_dim_info: str, *, include_inherited: bool = True 426) -> defaultdict[str, Any]: 427 """Read a metadata field for each value column of a single-metric frame. 428 429 For "value", search column, metric, shared and inherited metadata (if enabled), in that 430 order. Other columns use their own and shared metadata. 431 432 Args: 433 singletsframe: DataFrame with one metric's metadata in attrs. 434 value_dim_info: Field name or dotted path, e.g. "sensor.model". 435 include_inherited: Use inherited fields last, only for the "value" column. 436 437 Returns: 438 defaultdict[str, Any]: Field values as result[column]; unresolved fields are None. 439 440 Code example: 441 442 .. doctest:: metadata.get_singlets_info 443 444 >>> from hdhelpers.metadata.conventions import SINGLETSFRAME_METADATA_EXAMPLE 445 >>> dataframe.attrs = SINGLETSFRAME_METADATA_EXAMPLE 446 >>> md.get_singlets_info(dataframe, "name")["state"] 447 'measurement state' 448 449 See :ref:`metadata-getter-fields` for shared examples and return shapes. 450 """ 451 return _single_metric_by_column(singletsframe, value_dim_info, include_inherited)
452 453
[docs] 454def get_singlets_units( 455 singletsframe: pd.DataFrame, *, include_inherited: bool = False 456) -> defaultdict[str, str | None]: 457 """Read units for each value column of a single-metric frame. 458 459 Args: 460 singletsframe: DataFrame with one metric's metadata in attrs. 461 include_inherited: Use inherited fields last, only for the "value" column. 462 463 Returns: 464 defaultdict[str, str | None]: 465 Units as result[column]; "" means dimensionless and None means unavailable. 466 467 Code example: 468 469 .. doctest:: metadata.get_singlets_units 470 471 >>> from hdhelpers.metadata.conventions import SINGLETSFRAME_METADATA_EXAMPLE 472 >>> dataframe.attrs = SINGLETSFRAME_METADATA_EXAMPLE 473 >>> md.get_singlets_units(dataframe)["value"] 474 '°C' 475 476 See :ref:`metadata-getter-values` for shared examples and return shapes. 477 """ 478 return _single_metric_by_column(singletsframe, "unit", include_inherited)
479 480
[docs] 481def get_singlets_names( 482 singletsframe: pd.DataFrame, *, include_inherited: bool = False 483) -> defaultdict[str, str | None]: 484 """Read metadata names for each value column of a single-metric frame. 485 486 For "value", try the metric's name after the column's own name. 487 488 Args: 489 singletsframe: DataFrame with one metric's metadata in attrs. 490 include_inherited: Use inherited fields last, only for the "value" column. 491 492 Returns: 493 defaultdict[str, str | None]: Names as result[column]; unavailable names are None. 494 495 See :ref:`metadata-getter-names` for shared examples and return shapes. 496 """ 497 return _single_metric_by_column(singletsframe, "name", include_inherited)
498 499
[docs] 500def get_singlets_display_names( 501 singletsframe: pd.DataFrame, *, include_inherited: bool = False 502) -> defaultdict[str, str | None]: 503 """Read display labels for each value column of a single-metric frame. 504 505 Use display_name, then name. 506 507 Args: 508 singletsframe: DataFrame with one metric's metadata in attrs. 509 include_inherited: Use inherited fields last, only for the "value" column. 510 511 Returns: 512 defaultdict[str, str | None]: Labels as result[column]; unavailable labels are None. 513 514 See :ref:`metadata-getter-names` for shared examples and return shapes. 515 """ 516 return _single_metric_by_column(singletsframe, DISPLAY_NAME_FIELDS, include_inherited)
517 518
[docs] 519def get_singlets_short_display_names( 520 singletsframe: pd.DataFrame, *, include_inherited: bool = False 521) -> defaultdict[str, str | None]: 522 """Read short display labels for each value column of a single-metric frame. 523 524 Use short_display_name, then display_name, then name. 525 526 Args: 527 singletsframe: DataFrame with one metric's metadata in attrs. 528 include_inherited: Use inherited fields last, only for the "value" column. 529 530 Returns: 531 defaultdict[str, str | None]: Labels as result[column]; unavailable labels are None. 532 533 See :ref:`metadata-getter-names` for shared examples and return shapes. 534 """ 535 return _single_metric_by_column(singletsframe, SHORT_DISPLAY_NAME_FIELDS, include_inherited)
536 537
[docs] 538def get_singlets_measurements( 539 singletsframe: pd.DataFrame, *, include_inherited: bool = False 540) -> defaultdict[str, str | None]: 541 """Read physical quantities for each value column of a single-metric frame. 542 543 For example, "temperature" is a measurement and "°C" is its unit. 544 545 Args: 546 singletsframe: DataFrame with one metric's metadata in attrs. 547 include_inherited: Use inherited fields last, only for the "value" column. 548 549 Returns: 550 defaultdict[str, str | None]: 551 Quantities as result[column]; unavailable quantities are None. 552 553 See :ref:`metadata-getter-values` for shared examples and return shapes. 554 """ 555 return _single_metric_by_column(singletsframe, "measurement", include_inherited)
556 557
[docs] 558def get_singlets_metric_info( 559 singletsframe: pd.DataFrame, metric_info: str, *, include_inherited: bool = True 560) -> Any: 561 """Read one metadata field for the frame's single metric. 562 563 Search the metric, its "value" column, shared "value" metadata, then inherited fields 564 (if enabled). 565 566 Args: 567 singletsframe: DataFrame with one metric's metadata in attrs. 568 metric_info: Field name or dotted path, e.g. "sensor.model". 569 include_inherited: Use inherited fields as a last fallback. 570 571 Returns: 572 Any: One field value, or None when unavailable. 573 574 Code example: 575 576 Using :ref:`metadata-example-data`, compare a field of the metric with fields of its value 577 dimensions. See :ref:`metadata-getter-fields` for the related getters: 578 579 .. doctest:: metadata.get_singlets_metric_info 580 581 >>> md.get_singlets_metric_info(dataframe, "name") 582 'ABC temperature' 583 >>> md.get_singlets_info(dataframe, "name")["state"] 584 'measurement state' 585 """ 586 check_dataframe(singletsframe) 587 return _single_metric_or_dataset( 588 singletsframe, 589 "SINGLETSFRAME", 590 lambda meta, metric: meta.metric_info( 591 metric_info, metric, include_inherited=include_inherited 592 ), 593 lambda _meta: None, 594 empty=None, 595 )
596 597 598# queried interval, time zones, recording and aggregation 599 600
[docs] 601def get_queried_interval( 602 data: pd.Series | pd.DataFrame, 603) -> tuple[datetime.datetime | None, datetime.datetime | None]: 604 """Read the requested UTC time interval from dataset metadata. 605 606 Args: 607 data: Series or DataFrame with dataset metadata in attrs. 608 609 Returns: 610 tuple[datetime.datetime | None, datetime.datetime | None]: 611 (start, end) as timezone-aware datetimes, or (None, None) if unavailable. 612 613 Code example: 614 615 .. doctest:: metadata.get_queried_interval 616 617 >>> from hdhelpers.metadata.conventions import SERIES_METADATA_EXAMPLE 618 >>> series.attrs = SERIES_METADATA_EXAMPLE 619 >>> start, end = md.get_queried_interval(series) 620 >>> start.isoformat() 621 '2026-01-01T00:00:00+00:00' 622 623 See :ref:`metadata-getter-values` for shared examples and return shapes. 624 """ 625 check_series_or_dataframe(data) 626 meta = _metadata(data, infer_data_type(data)) 627 return (None, None) if meta is None else meta.queried_interval()
628 629
[docs] 630def get_default_timezones(multitsframe: pd.DataFrame) -> defaultdict[str, str | None]: 631 """Read the time zone recommended for calculations on each metric. 632 633 Use the metric's default_timezone, then the dataset's default_timezone. 634 635 Args: 636 multitsframe: DataFrame with per-metric metadata in attrs. 637 638 Returns: 639 defaultdict[str, str | None]: 640 Zone names or UTC offsets by metric ID, or None for unresolved zones. Missing 641 IDs use the dataset default. 642 643 Code example: 644 645 .. doctest:: metadata.get_default_timezones 646 647 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 648 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 649 >>> md.get_default_timezones(multitsframe)["plant_a.pressure"] 650 'Europe/Berlin' 651 652 See :ref:`metadata-getter-timezones` for shared examples and return shapes. 653 """ 654 check_dataframe(multitsframe) 655 meta = _metadata(multitsframe, "MULTITSFRAME") 656 if meta is None: 657 return defaultdict(lambda: None) 658 dataset_timezone = dataset_default_timezone(meta) 659 return defaultdict(lambda: dataset_timezone, meta.default_timezones())
660 661
[docs] 662def get_series_default_timezone(series: pd.Series) -> str | None: 663 """Read the time zone recommended for calculations on this Series. 664 665 Use the metric's default_timezone, then the dataset's default_timezone. 666 667 Args: 668 series: Series with metadata in attrs. 669 670 Returns: 671 str | None: Zone name or UTC offset, or None when neither source provides one. 672 673 See :ref:`metadata-getter-timezones` for shared examples and return shapes. 674 """ 675 check_series(series) 676 return _single_metric_or_dataset( # type: ignore[no-any-return] 677 series, 678 "SERIES", 679 lambda meta, metric: meta.default_timezone(metric), 680 dataset_default_timezone, 681 empty=None, 682 )
683 684
[docs] 685def get_singlets_default_timezone(singletsframe: pd.DataFrame) -> str | None: 686 """Read the time zone recommended for calculations on a single-metric frame. 687 688 Use the metric's default_timezone, then the dataset's default_timezone. 689 690 Args: 691 singletsframe: DataFrame with one metric's metadata in attrs. 692 693 Returns: 694 str | None: Zone name or UTC offset, or None when neither source provides one. 695 696 See :ref:`metadata-getter-timezones` for shared examples and return shapes. 697 """ 698 check_dataframe(singletsframe) 699 return _single_metric_or_dataset( # type: ignore[no-any-return] 700 singletsframe, 701 "SINGLETSFRAME", 702 lambda meta, metric: meta.default_timezone(metric), 703 dataset_default_timezone, 704 empty=None, 705 )
706 707
[docs] 708def get_recordings(multitsframe: pd.DataFrame) -> defaultdict[str, Recording]: 709 """Read recording settings for each metric. 710 711 Use the metric's frequency, offset and time zone together. If its frequency is missing, 712 use the dataset's three fields instead. 713 714 Args: 715 multitsframe: DataFrame with per-metric metadata in attrs. 716 717 Returns: 718 defaultdict[str, Recording]: 719 Settings by metric ID; missing IDs use dataset frequency settings. 720 721 Code example: 722 723 .. doctest:: metadata.get_recordings 724 725 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 726 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 727 >>> md.get_recordings(multitsframe)["plant_a.temperature"].recording_mode 728 'on_change' 729 730 See :ref:`metadata-getter-recordings` for shared examples and return shapes. 731 """ 732 check_dataframe(multitsframe) 733 meta = _metadata(multitsframe, "MULTITSFRAME") 734 if meta is None: 735 return defaultdict(Recording) 736 no_metadata = dataset_recording(meta) 737 return defaultdict(lambda: no_metadata, meta.recordings())
738 739
[docs] 740def get_series_recording(series: pd.Series) -> Recording: 741 """Read recording settings for this Series. 742 743 Use the metric's frequency, offset and time zone together. If its frequency is missing, 744 use the dataset's three fields instead. 745 746 Args: 747 series: Series with metadata in attrs. 748 749 Returns: 750 Recording: Recording object; absent metadata yields its default field values. 751 752 See :ref:`metadata-getter-recordings` for shared examples and return shapes. 753 """ 754 check_series(series) 755 return _single_metric_or_dataset( # type: ignore[no-any-return] 756 series, 757 "SERIES", 758 lambda meta, metric: meta.recording(metric), 759 dataset_recording, 760 empty=Recording(), 761 )
762 763
[docs] 764def get_singlets_recording(singletsframe: pd.DataFrame) -> Recording: 765 """Read recording settings for a single-metric frame. 766 767 Use the metric's frequency, offset and time zone together. If its frequency is missing, 768 use the dataset's three fields instead. 769 770 Args: 771 singletsframe: DataFrame with one metric's metadata in attrs. 772 773 Returns: 774 Recording: Recording object; absent metadata yields its default field values. 775 776 See :ref:`metadata-getter-recordings` for shared examples and return shapes. 777 """ 778 check_dataframe(singletsframe) 779 return _single_metric_or_dataset( # type: ignore[no-any-return] 780 singletsframe, 781 "SINGLETSFRAME", 782 lambda meta, metric: meta.recording(metric), 783 dataset_recording, 784 empty=Recording(), 785 )
786 787
[docs] 788def get_default_aggregation_methods(multitsframe: pd.DataFrame) -> defaultdict[str, str]: 789 """Read the default aggregation method for each metric. 790 791 Use explicit default_aggregation_method (AVG becomes MEAN). Otherwise, metadata 792 value_data_type selects LAST for boolean/string and MEAN for other types. 793 794 Args: 795 multitsframe: DataFrame with per-metric metadata in attrs. 796 797 Returns: 798 defaultdict[str, str]: Method names by metric ID; missing IDs default to MEAN. 799 800 Code example: 801 802 .. doctest:: metadata.get_default_aggregation_methods 803 804 >>> from hdhelpers.metadata.conventions import MULTITSFRAME_METADATA_EXAMPLE 805 >>> multitsframe.attrs = MULTITSFRAME_METADATA_EXAMPLE 806 >>> methods = md.get_default_aggregation_methods(multitsframe) 807 >>> methods["plant_a.temperature"], methods["plant_a.pressure"] 808 ('MAX', 'MEAN') 809 810 See :ref:`metadata-getter-aggregation` for shared examples and return shapes. 811 """ 812 check_dataframe(multitsframe) 813 meta = _metadata(multitsframe, "MULTITSFRAME") 814 methods = {} if meta is None else meta.default_aggregation_methods() 815 return defaultdict(lambda: _DEFAULT_AGGREGATION_METHOD, methods)
816 817
[docs] 818def get_series_default_aggregation_method(series: pd.Series) -> str: 819 """Read the default aggregation method for this Series. 820 821 Use explicit default_aggregation_method (AVG becomes MEAN). Otherwise, metadata 822 value_data_type selects LAST for boolean/string and MEAN for other types. 823 824 Args: 825 series: Series with metadata in attrs. 826 827 Returns: 828 str: Method name; MEAN if metadata is unavailable. 829 830 See :ref:`metadata-getter-aggregation` for shared examples and return shapes. 831 """ 832 check_series(series) 833 return _single_metric_or_dataset( # type: ignore[no-any-return] 834 series, 835 "SERIES", 836 lambda meta, metric: meta.default_aggregation_method(metric), 837 lambda _meta: _DEFAULT_AGGREGATION_METHOD, 838 empty=_DEFAULT_AGGREGATION_METHOD, 839 )
840 841
[docs] 842def get_singlets_default_aggregation_method(singletsframe: pd.DataFrame) -> str: 843 """Read the default aggregation method for a single-metric frame. 844 845 Use explicit default_aggregation_method (AVG becomes MEAN). Otherwise, metadata 846 value_data_type selects LAST for boolean/string and MEAN for other types. 847 848 Args: 849 singletsframe: DataFrame with one metric's metadata in attrs. 850 851 Returns: 852 str: Method name; MEAN if metadata is unavailable. 853 854 See :ref:`metadata-getter-aggregation` for shared examples and return shapes. 855 """ 856 check_dataframe(singletsframe) 857 return _single_metric_or_dataset( # type: ignore[no-any-return] 858 singletsframe, 859 "SINGLETSFRAME", 860 lambda meta, metric: meta.default_aggregation_method(metric), 861 lambda _meta: _DEFAULT_AGGREGATION_METHOD, 862 empty=_DEFAULT_AGGREGATION_METHOD, 863 )
864 865
[docs] 866def copy_without_metadata[T: (pd.Series, pd.DataFrame)](data: T) -> T: 867 """Copy a Series or DataFrame with all attrs removed. 868 869 Data is shared until either object is modified (pandas copy on write). 870 871 Args: 872 data (pd.Series | pd.DataFrame): Source object; left unchanged. 873 874 Returns: 875 pd.Series | pd.DataFrame: Copy with empty attrs. 876 877 Code example: 878 879 .. doctest:: metadata.copy_without_metadata 880 881 >>> multitsframe.attrs = {"dataset_metadata": {"metric_key": "id"}, "metrics": []} 882 >>> data = md.copy_without_metadata(multitsframe) 883 >>> data.attrs 884 {} 885 >>> multitsframe.attrs 886 {'dataset_metadata': {'metric_key': 'id'}, 'metrics': []} 887 """ 888 check_series_or_dataframe(data) 889 # The constructor does not copy attrs, in contrast to e.g. copy(deep=False) 890 copy = type(data)(data) 891 copy.attrs = {} 892 return copy