Skip to content

Commit d18754a

Browse files
slopez-bnathan-stender
authored andcommitted
feat: Luminex xPONENT - add errors for nan and missing values in statistics tables (#1036)
1 parent 4e59b4e commit d18754a

6 files changed

Lines changed: 21489 additions & 90 deletions

File tree

src/allotropy/parsers/luminex_xponent/luminex_xponent_reader.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,15 @@ def _get_results(reader: CsvReader) -> dict[str, pd.DataFrame]:
9191
result_title = match.groups()[0]
9292
table_data = assert_not_none(
9393
reader.pop_csv_block_as_df(
94-
empty_pat=constants.LUMINEX_EMPTY_PATTERN, header=0, index_col=0
94+
empty_pat=constants.LUMINEX_EMPTY_PATTERN,
95+
header=0,
96+
index_col=0,
97+
na_values=["NaN", "nan", "-NaN", "-nan", "None", "null", "NULL"],
98+
keep_default_na=False, # Prevent pandas from interpreting empty strings as NaN.
9599
)
96-
).dropna(how="all", axis="columns")
100+
)
101+
# drop empty columns at the end of the table
102+
table_data = table_data.loc[:, ~table_data.columns.str.contains("^Unnamed")]
97103
results[result_title] = table_data
98104
reader.drop_empty(constants.LUMINEX_EMPTY_PATTERN)
99105

src/allotropy/parsers/luminex_xponent/luminex_xponent_structure.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import itertools
55
from pathlib import Path
66
import re
7+
from typing import Any
78

89
import pandas as pd
910

@@ -39,7 +40,12 @@
3940
)
4041
from allotropy.parsers.utils.pandas import map_rows, SeriesData
4142
from allotropy.parsers.utils.uuids import random_uuid_str
42-
from allotropy.parsers.utils.values import assert_not_none, try_float
43+
from allotropy.parsers.utils.values import (
44+
assert_not_none,
45+
try_float,
46+
try_non_nan_float_or_negative_zero,
47+
try_non_nan_float_or_none,
48+
)
4349

4450

4551
@dataclass(frozen=True)
@@ -163,6 +169,7 @@ def create(
163169
location = str(count_data.series.name)
164170
dilution_factor_data = results_data["Dilution Factor"]
165171
errors_data = results_data.get("Warnings/Errors")
172+
measurement_identifier = random_uuid_str()
166173

167174
if location not in dilution_factor_data.index:
168175
msg = f"Could not find 'Dilution Factor' data for: '{location}'."
@@ -177,31 +184,49 @@ def create(
177184
)
178185
errors: list[Error] = []
179186
data_errors = cls._get_errors(errors_data, well_location) or []
180-
for error in data_errors:
181-
errors.append(Error(error=error))
187+
for data_error in data_errors:
188+
errors.append(Error(error=data_error))
182189

183190
if dilution_factor_setting == NEGATIVE_ZERO:
184191
errors.append(
185192
Error(error="Not reported in file", feature="dilution factor setting")
186193
)
187194

195+
def get_statistics_error(
196+
raw_value: Any, analyte: str, role: str
197+
) -> Error | None:
198+
"""Check if the raw value is empty or NaN and return an Error if so."""
199+
error = None
200+
if raw_value == "":
201+
error = "Not reported"
202+
elif try_non_nan_float_or_none(raw_value) is None:
203+
error = "NaN"
204+
return Error(error=error, feature=f"{analyte} <{role}>") if error else None
205+
188206
def get_statistic_dimensions(analyte: str) -> list[StatisticDimension]:
189-
return [
190-
StatisticDimension(
191-
value=try_float(statistic_table.at[location, analyte], analyte),
192-
unit=statistic_conf.unit,
193-
statistic_datum_role=statistic_conf.role,
207+
statistic_dimensions = []
208+
for section, statistic_conf in STATISTIC_SECTIONS_CONF.items():
209+
if (statistic_table := results_data.get(section)) is None:
210+
continue
211+
raw_value = statistic_table.at[location, analyte]
212+
role = statistic_conf.role
213+
if error := get_statistics_error(raw_value, analyte, role.value):
214+
errors.append(error)
215+
statistic_dimensions.append(
216+
StatisticDimension(
217+
value=try_non_nan_float_or_negative_zero(raw_value),
218+
unit=statistic_conf.unit,
219+
statistic_datum_role=role,
220+
)
194221
)
195-
for section, statistic_conf in STATISTIC_SECTIONS_CONF.items()
196-
if (statistic_table := results_data.get(section)) is not None
197-
]
222+
return statistic_dimensions
198223

199-
measurement_id = random_uuid_str()
200224
analytes = []
201225
calculated_data = []
202-
for analyte in [
226+
analyte_keys = [
203227
key for key in count_data.series.index if key not in metadata_keys
204-
]:
228+
]
229+
for analyte in analyte_keys:
205230
analytes.append(
206231
Analyte(
207232
identifier=(analyte_identifier := random_uuid_str()),
@@ -217,14 +242,21 @@ def get_statistic_dimensions(analyte: str) -> list[StatisticDimension]:
217242
)
218243
)
219244

220-
calculated_data.extend(
221-
[
245+
for section_name, unit in CALCULATED_DATA_SECTIONS.items():
246+
calculated_data_section = results_data.get(section_name, pd.DataFrame())
247+
# Some sections don't include the location as an index, in those cases we cannot associate
248+
# the calculated data with the measurement.
249+
if location not in calculated_data_section.index:
250+
continue
251+
raw_value = calculated_data_section.at[location, analyte]
252+
if error := get_statistics_error(raw_value, analyte, section_name):
253+
errors.append(error)
254+
calculated_data_id = random_uuid_str()
255+
calculated_data.append(
222256
CalculatedDocument(
223-
uuid=random_uuid_str(),
257+
uuid=calculated_data_id,
224258
name=section_name,
225-
value=try_float(
226-
calculated_data_section.at[location, analyte], analyte
227-
),
259+
value=try_non_nan_float_or_negative_zero(raw_value),
228260
unit=unit,
229261
data_sources=[
230262
DataSource(
@@ -233,15 +265,10 @@ def get_statistic_dimensions(analyte: str) -> list[StatisticDimension]:
233265
)
234266
],
235267
)
236-
for section_name, unit in CALCULATED_DATA_SECTIONS.items()
237-
if section_name in results_data
238-
and location
239-
in (calculated_data_section := results_data[section_name]).index
240-
]
241-
)
268+
)
242269

243270
return Measurement(
244-
identifier=measurement_id,
271+
identifier=measurement_identifier,
245272
sample_identifier=count_data[str, "Sample"],
246273
location_identifier=location_id,
247274
dilution_factor_setting=dilution_factor_setting,

src/allotropy/parsers/utils/values.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
TStatisticDatumRole,
1313
)
1414
from allotropy.exceptions import AllotropeConversionError, AllotropyParserError
15+
from allotropy.parsers.constants import NEGATIVE_ZERO
1516
from allotropy.parsers.utils.units import get_quantity_class
1617

1718
PrimitiveValue = str | int | float
@@ -90,6 +91,11 @@ def try_non_nan_float(value: str) -> float:
9091
return float_value
9192

9293

94+
def try_non_nan_float_or_negative_zero(value: str) -> float:
95+
float_value = try_non_nan_float_or_none(value)
96+
return NEGATIVE_ZERO if float_value is None else float_value
97+
98+
9399
def try_int_or_nan(value: str | int | None) -> int | InvalidJsonFloat:
94100
if isinstance(value, int):
95101
return value

0 commit comments

Comments
 (0)