Skip to content

Commit ac61935

Browse files
authored
Improve error handling for data loading and transformation (#21)
1 parent 5f8274a commit ac61935

8 files changed

Lines changed: 106 additions & 33 deletions

File tree

TODO.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ After completing a milestone, create a pull request with your changes for review
181181
- [x] Integrated evaluation metrics and plots into the Data Explorer page
182182
- [x] Implemented modeling page with model selection, training, cross-validation, and export functionality
183183

184+
## PR17: Robust Error Handling
185+
186+
- [x] Improve data-loading functions to validate inputs and raise descriptive errors
187+
- [x] Add checks for missing columns and types in transformation utilities
188+
- [x] Surface error messages in UI pages using `st.error`
189+
- [x] Add tests for new error handling in data and transform modules
190+
184191
## Notes for Development
185192

186193
- Create comprehensive commit messages that clearly describe changes

pages/data_explorer.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ def main() -> None:
4545
st.subheader("Sample Datasets")
4646
for name, path in config.SAMPLE_DATASETS.items():
4747
if st.button(f"Load {name}"):
48-
st.session_state["data"] = data_utils.load_data(path)
49-
st.session_state["data"] = data_utils.convert_dtypes(
50-
st.session_state["data"]
51-
)
52-
st.session_state["datetime_cols"] = eda.detect_datetime_columns(
53-
st.session_state["data"]
54-
)
55-
st.success(f"{name} loaded!")
48+
try:
49+
st.session_state["data"] = data_utils.load_data(path)
50+
st.session_state["data"] = data_utils.convert_dtypes(
51+
st.session_state["data"]
52+
)
53+
st.session_state["datetime_cols"] = eda.detect_datetime_columns(
54+
st.session_state["data"]
55+
)
56+
st.success(f"{name} loaded!")
57+
except (ValueError, TypeError) as exc:
58+
st.error(f"Failed to load sample data: {exc}")
5659

5760
with st.expander("Help"):
5861
st.markdown(ui.help_markdown())
@@ -64,7 +67,7 @@ def main() -> None:
6467
st.session_state["data"] = df
6568
st.session_state["datetime_cols"] = eda.detect_datetime_columns(df)
6669
st.success("File loaded successfully!")
67-
except ValueError as exc:
70+
except (ValueError, TypeError) as exc:
6871
st.error(f"Failed to load file: {exc}")
6972

7073
data = st.session_state.get("data")
@@ -171,23 +174,27 @@ def _corr(df):
171174
)
172175
if st.button("Apply Transformations"):
173176
df_trans = data.copy()
174-
if missing_strategy == "Drop rows":
175-
df_trans = transform.handle_missing_values(df_trans, strategy="drop")
176-
elif missing_strategy == "Fill Mean":
177-
df_trans = transform.handle_missing_values(df_trans, strategy="mean")
178-
elif missing_strategy == "Fill Median":
179-
df_trans = transform.handle_missing_values(df_trans, strategy="median")
180-
elif missing_strategy == "Fill Mode":
181-
df_trans = transform.handle_missing_values(df_trans, strategy="mode")
182-
if encode_cols:
183-
method = "onehot" if encode_method == "One-Hot" else "label"
184-
df_trans = transform.encode_features(df_trans, encode_cols, method=method)
185-
if scale_cols:
186-
method = "standard" if scale_method == "Standard" else "minmax"
187-
df_trans = transform.scale_features(df_trans, scale_cols, method=method)
188-
st.session_state["data"] = df_trans
189-
data = df_trans
190-
st.success("Transformations applied!")
177+
try:
178+
if missing_strategy == "Drop rows":
179+
df_trans = transform.handle_missing_values(df_trans, strategy="drop")
180+
elif missing_strategy == "Fill Mean":
181+
df_trans = transform.handle_missing_values(df_trans, strategy="mean")
182+
elif missing_strategy == "Fill Median":
183+
df_trans = transform.handle_missing_values(df_trans, strategy="median")
184+
elif missing_strategy == "Fill Mode":
185+
df_trans = transform.handle_missing_values(df_trans, strategy="mode")
186+
if encode_cols:
187+
method = "onehot" if encode_method == "One-Hot" else "label"
188+
df_trans = transform.encode_features(df_trans, encode_cols, method=method)
189+
if scale_cols:
190+
method = "standard" if scale_method == "Standard" else "minmax"
191+
df_trans = transform.scale_features(df_trans, scale_cols, method=method)
192+
except (ValueError, KeyError, TypeError) as exc:
193+
st.error(f"Transformation error: {exc}")
194+
else:
195+
st.session_state["data"] = df_trans
196+
data = df_trans
197+
st.success("Transformations applied!")
191198

192199
st.subheader("Model Training - Classification")
193200
target = st.selectbox(

pages/prediction.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ def main() -> None:
3333
model_obj = predict.load_model(Path(tmp.name))
3434

3535
if data_file is not None:
36-
df = data_utils.load_data(data_file)
37-
df = data_utils.convert_dtypes(df)
38-
st.session_state["pred_data"] = df
36+
try:
37+
df = data_utils.load_data(data_file)
38+
df = data_utils.convert_dtypes(df)
39+
st.session_state["pred_data"] = df
40+
except (ValueError, TypeError) as exc:
41+
st.error(f"Failed to load data: {exc}")
3942

4043
data = st.session_state.get("pred_data")
4144

pages/report.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ def main() -> None:
2626
)
2727

2828
if data_file is not None:
29-
df = data_utils.load_data(data_file)
30-
df = data_utils.convert_dtypes(df)
31-
st.session_state["report_data"] = df
29+
try:
30+
df = data_utils.load_data(data_file)
31+
df = data_utils.convert_dtypes(df)
32+
st.session_state["report_data"] = df
33+
except (ValueError, TypeError) as exc:
34+
st.error(f"Failed to load data: {exc}")
3235

3336
df = st.session_state.get("report_data")
3437
if df is not None:

tests/test_data_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,18 @@ def test_sample_dataset_loads():
4646
df = data.load_data(path)
4747
assert not df.empty
4848

49+
50+
def test_load_data_invalid_type():
51+
with pytest.raises(ValueError):
52+
data.load_data(123)
53+
54+
55+
def test_convert_dtypes_invalid():
56+
with pytest.raises(TypeError):
57+
data.convert_dtypes([1, 2, 3])
58+
59+
60+
def test_data_summary_empty():
61+
with pytest.raises(ValueError):
62+
data.data_summary(pd.DataFrame())
63+

tests/test_transform.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,15 @@ def test_transformation_workflow():
5555
df = transform.scale_features(df, ["num"], method="minmax")
5656
assert df["num"].min() == 0
5757
assert "cat" in df.columns
58+
59+
60+
def test_encode_features_missing_column():
61+
df = sample_df().fillna({"cat": "b"})
62+
with pytest.raises(KeyError):
63+
transform.encode_features(df, ["missing"], method="onehot")
64+
65+
66+
def test_scale_features_non_numeric():
67+
df = sample_df().fillna({"cat": "b"})
68+
with pytest.raises(TypeError):
69+
transform.scale_features(df, ["cat"], method="standard")

utils/data.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010

1111
def load_data(file: Any) -> pd.DataFrame:
1212
"""Load a CSV or Excel file into a DataFrame."""
13+
if file is None:
14+
raise ValueError("No file provided")
15+
1316
if hasattr(file, "name"):
1417
ext = Path(file.name).suffix.lower()
1518
else:
16-
ext = Path(str(file)).suffix.lower()
19+
path = Path(str(file))
20+
ext = path.suffix.lower()
21+
if not path.exists():
22+
raise ValueError("Invalid file path")
1723

1824
try:
1925
if ext == ".csv":
@@ -27,6 +33,8 @@ def load_data(file: Any) -> pd.DataFrame:
2733

2834
def convert_dtypes(df: pd.DataFrame) -> pd.DataFrame:
2935
"""Attempt to convert object columns to numeric or datetime."""
36+
if not isinstance(df, pd.DataFrame):
37+
raise TypeError("df must be a pandas DataFrame")
3038
for column in df.columns:
3139
if df[column].dtype == object:
3240
df[column] = pd.to_numeric(df[column], errors="ignore")
@@ -37,4 +45,8 @@ def convert_dtypes(df: pd.DataFrame) -> pd.DataFrame:
3745

3846
def data_summary(df: pd.DataFrame) -> pd.DataFrame:
3947
"""Return a statistical summary of the dataframe."""
48+
if not isinstance(df, pd.DataFrame):
49+
raise TypeError("df must be a pandas DataFrame")
50+
if df.empty:
51+
raise ValueError("DataFrame is empty")
4052
return df.describe(include="all")

utils/transform.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
def handle_missing_values(df: pd.DataFrame, *, strategy: str = "drop") -> pd.DataFrame:
1717
"""Handle missing values according to the specified strategy."""
18+
if not isinstance(df, pd.DataFrame):
19+
raise TypeError("df must be a pandas DataFrame")
1820
if strategy not in MISSING_STRATEGIES:
1921
raise ValueError(f"Invalid strategy: {strategy}")
2022
if strategy == "drop":
@@ -40,6 +42,11 @@ def encode_features(
4042
method: str = "onehot",
4143
) -> pd.DataFrame:
4244
"""Encode categorical features using the given method."""
45+
if not isinstance(df, pd.DataFrame):
46+
raise TypeError("df must be a pandas DataFrame")
47+
missing = set(columns) - set(df.columns)
48+
if missing:
49+
raise KeyError(f"Columns not found: {', '.join(missing)}")
4350
if method not in ENCODING_METHODS:
4451
raise ValueError(f"Invalid encoding method: {method}")
4552
df = df.copy()
@@ -58,6 +65,13 @@ def scale_features(
5865
method: str = "standard",
5966
) -> pd.DataFrame:
6067
"""Scale numeric features with the given method."""
68+
if not isinstance(df, pd.DataFrame):
69+
raise TypeError("df must be a pandas DataFrame")
70+
missing = set(columns) - set(df.columns)
71+
if missing:
72+
raise KeyError(f"Columns not found: {', '.join(missing)}")
73+
if not all(pd.api.types.is_numeric_dtype(df[c]) for c in columns):
74+
raise TypeError("Scale features require numeric columns")
6175
if method not in SCALING_METHODS:
6276
raise ValueError(f"Invalid scaling method: {method}")
6377
df = df.copy()

0 commit comments

Comments
 (0)