Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/data_manager.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
"""Data loading, export, and column management."""

import io
import logging
import pandas as pd
import numpy as np
import streamlit as st
from core.state import set_df, get_df, get_var_types, set_var_type
from core import constants

logger = logging.getLogger(__name__)


def load_csv(uploaded_file):
"""Load data from a CSV file."""
try:
df = pd.read_csv(uploaded_file, low_memory=False)
_apply_loaded_df(df)
return True, None
except ValueError as e:
return False, str(e)
except Exception:
return False, "Unable to load CSV file. Please check the format."

Expand All @@ -24,6 +29,8 @@ def load_excel(uploaded_file):
df = pd.read_excel(uploaded_file)
_apply_loaded_df(df)
return True, None
except ValueError as e:
return False, str(e)
except Exception:
return False, "Unable to load Excel file. Please check the format."

Expand All @@ -41,12 +48,24 @@ def load_from_paste(text):
df = pd.read_csv(io.StringIO(text), sep=";")
_apply_loaded_df(df)
return True, None
except ValueError as e:
return False, str(e)
except Exception:
return False, "Unable to parse pasted data. Please check the format."


def _apply_loaded_df(df):
"""Apply a loaded DataFrame to session state."""
# Validate minimum data dimensions
if len(df.columns) == 0:
logger.warning("Malformed upload: shape=%s", df.shape)
raise ValueError("Uploaded data is empty or has no columns.")

if len(df) == 0:
logger.warning("No data rows in upload, only columns: %s", list(df.columns))

logger.debug("Loaded DataFrame: shape=%s, columns=%s", df.shape, list(df.columns))

# Pad with empty rows so user can add more data
if len(df) < 20:
extra = pd.DataFrame(
Expand All @@ -65,6 +84,7 @@ def _auto_detect_type(col, df):
"""Auto-detect variable type for a column."""
series = df[col].dropna()
if len(series) == 0:
logger.debug("Column '%s': empty, assigned METRIC", col)
set_var_type(col, constants.METRIC)
return

Expand All @@ -75,13 +95,17 @@ def _auto_detect_type(col, df):
if non_null_numeric / len(series) > 0.5:
n_unique = numeric.dropna().nunique()
if n_unique <= 2:
logger.debug("Column '%s': %d unique numeric values, assigned NOMINAL", col, n_unique)
set_var_type(col, constants.NOMINAL)
elif n_unique <= 7 and n_unique < len(series) * 0.3:
logger.debug("Column '%s': %d unique numeric values (sparse), assigned ORDINAL", col, n_unique)
set_var_type(col, constants.ORDINAL)
else:
logger.debug("Column '%s': %d unique numeric values, assigned METRIC", col, n_unique)
set_var_type(col, constants.METRIC)
else:
# Non-numeric columns are Nominal regardless of cardinality.
logger.debug("Column '%s': non-numeric (%.1f%% coercible), assigned NOMINAL", col, 100 * non_null_numeric / len(series))
set_var_type(col, constants.NOMINAL)


Expand Down
29 changes: 28 additions & 1 deletion tests/test_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import streamlit as st

from core import constants
from core.data_manager import _auto_detect_type, add_column, load_csv
from core.data_manager import _apply_loaded_df, _auto_detect_type, add_column, load_csv


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -74,6 +74,33 @@ def test_load_csv_detects_every_column():
assert types["label"] == constants.NOMINAL


def test_load_csv_with_header_only_still_loads_and_pads_rows():
# A CSV with columns but no data rows (e.g. a fresh export template) must
# still load so the user can type values into the padded editor rows.
csv = "num,grp,label\n"
ok, err = load_csv(io.StringIO(csv))

assert (ok, err) == (True, None)
df = st.session_state["df"]
assert list(df.columns) == ["num", "grp", "label"]
assert len(df) == 20


def test_apply_loaded_df_rejects_dataframe_with_no_columns():
with pytest.raises(ValueError):
_apply_loaded_df(pd.DataFrame())


def test_load_csv_with_no_columns_reports_specific_error_not_generic_one():
# A ValueError raised while applying the loaded frame must reach the
# caller verbatim instead of being swallowed by the catch-all Exception
# handler and replaced with the generic format-error message.
ok, err = load_csv(io.StringIO(""))

assert ok is False
assert err != "Unable to load CSV file. Please check the format."


def test_add_column_marks_the_new_column_as_metric():
add_column()

Expand Down
Loading