nbus_stream_parser.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import pandas as pd
  2. import numpy as np
  3. class DataParser:
  4. def __init__(self, dataframe: pd.DataFrame):
  5. self._df = dataframe
  6. def remap_columns(self, new_columns):
  7. self._df.rename(columns=new_columns, inplace=True)
  8. def remap_nans(self, new_nans):
  9. # Loop through the column names and their target float values
  10. for col, target_value in new_nans.items():
  11. # Check if the column exists in your DataFrame to prevent KeyErrors
  12. if col in self._df.columns:
  13. # 1. Convert the column to numeric type (strings/text will be coerced to NaN)
  14. self._df[col] = pd.to_numeric(self._df[col], errors='coerce')
  15. # 2. Create a safe float mask for just this column
  16. equal_to = np.isclose(
  17. self._df[col],
  18. target_value,
  19. atol=1e-8
  20. )
  21. # 3. Apply the mask and replace matching values with NaN in-place
  22. self._df.loc[equal_to, col] = np.nan
  23. def validate_and_filter_series_by_zeros(self, ser_col, pha_col, expected_phases):
  24. """
  25. Validates measurement series separated by zeros. If a series block between
  26. zeros does not contain the expected number of unique phases, the entire block is removed.
  27. Parameters:
  28. - ser_col (str): Column name for the series ID.
  29. - pha_col (str): Column name for the phase ID.
  30. - expected_phases (int): Required number of unique phases per series block (default: 6).
  31. """
  32. # 1. Identify rows that are zeros (separator rows)
  33. is_zero = (self._df[ser_col] == 0) & (self._df[pha_col] == 0)
  34. # 2. Create a unique group ID for each block of rows between zeros
  35. block_id = is_zero.cumsum()
  36. valid_indices = []
  37. # 3. Iterate through each block between zeros
  38. ser_no = 1
  39. for b_id, group in self._df.groupby(block_id):
  40. # Skip pure zero separator blocks (we can keep or drop them, usually they are separators)
  41. if is_zero.loc[group.index].all():
  42. continue
  43. # Check how many unique phases are in this specific block
  44. unique_phases = group[pha_col].nunique() - 1
  45. # If it matches the expected number of phases, keep its indices
  46. if unique_phases == expected_phases:
  47. self._df.loc[group.index, ser_col] = ser_no
  48. ser_no += 1
  49. valid_indices.extend(group.index.tolist())
  50. # 4. Keep only rows belonging to valid blocks
  51. invalid_indices = self._df.index.difference(valid_indices)
  52. self._df.drop(index=invalid_indices, inplace=True)
  53. def remove_specific_phases(self, pha_col, phases_to_remove):
  54. self._df.drop(
  55. index=self._df.index[
  56. self._df[pha_col].isin(phases_to_remove)
  57. ],
  58. inplace=True
  59. )
  60. def remap_phases(self, pha_col, phase_mapping):
  61. # Replace phase values according to the dictionary mapping
  62. if phase_mapping is not None:
  63. self._df[pha_col] = self._df[pha_col].replace(phase_mapping)
  64. def drop_rows_completely_null(self, columns):
  65. """
  66. Drops rows where ALL columns (except series and phase) are NaN simultaneously.
  67. """
  68. cols_to_check = [
  69. col for col in self._df.columns if col in columns
  70. ]
  71. self._df.dropna(
  72. subset=cols_to_check,
  73. how='all',
  74. inplace=True
  75. )
  76. def clean_precision_noise(self, precision=5):
  77. """
  78. Finds all float columns, rounds off float precision artifacts (e.g., 3.0000002 -> 3.0),
  79. and safely converts them using the 'g' specifier format so trailing zero decimals
  80. are completely dropped upon viewing or CSV export, while leaving real decimals intact.
  81. """
  82. # 1. Locate all float columns dynamically to avoid breaking string/text columns
  83. float_cols = self._df.select_dtypes(
  84. include=['float', 'float64']
  85. ).columns
  86. # 2. Round the target columns to snap the noisy precision bits to their actual values
  87. self._df[float_cols] = self._df[float_cols].round(precision)
  88. # 3. Format the cells to trim the trailing '.0' cleanly using g-formatting
  89. # (This is vectorized across selected fields and leaves NaNs unharmed)
  90. for col in float_cols:
  91. self._df[col] = self._df[col].map(
  92. lambda x: f"{x:g}" if pd.notnull(x) else x
  93. )
  94. def get_dataframe(self):
  95. return self._df