Skip to content

Commit ba3f01f

Browse files
Merge pull request #517 from satheesh142002/feat/511-streamlit-app
feat(examples): add Streamlit cleaning app recipe
2 parents 58577e5 + 8a2b78a commit ba3f01f

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

‎examples/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Located in [`examples/integrations/`](integrations/):
3838
| **DuckDB** | [`duckdb_workflow.py`](integrations/duckdb_workflow.py) | Direct cleaning of `DuckDBPyRelation` objects and registering back to DuckDB for SQL. |
3939
| **scikit-learn** | [`sklearn_pipeline.py`](integrations/sklearn_pipeline.py) | Custom `FreshDataCleaner` transformer for native `sklearn.pipeline.Pipeline` usage. |
4040
| **FastAPI** | [`fastapi_clean_service.py`](integrations/fastapi_clean_service.py) | Asynchronous cleaning microservice accepting CSV uploads or JSON records without blocking the event loop. |
41+
| **Streamlit** | [`streamlit_app.py`](integrations/streamlit_app.py) | Interactive CSV cleaning app with raw/cleaned previews, quality metrics, and a cleaned CSV download. |
4142
| **Apache Airflow** | [`airflow_task.py`](integrations/airflow_task.py) | Quality gating with `evaluate_trust_gate` and `FreshDataCleanOperator` DAG pattern. |
4243
| **Pandera** | [`09_pandera_recipe.py`](09_pandera_recipe.py) | Pre- and post-validation using declarative Pandera DataFrame schemas. |
4344
| **PyJanitor** | [`10_pyjanitor_interop.py`](10_pyjanitor_interop.py) | Combining explicit PyJanitor transforms with FreshData quality repair. |
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Interactive Streamlit data-cleaning recipe.
2+
3+
Install dependencies with::
4+
5+
pip install streamlit freshdata-cleaner
6+
7+
Run the app with::
8+
9+
streamlit run examples/integrations/streamlit_app.py
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from io import BytesIO
15+
from typing import Any
16+
17+
import pandas as pd
18+
import streamlit as st
19+
20+
import freshdata as fd
21+
22+
23+
def demo_data() -> pd.DataFrame:
24+
"""Return a small messy dataset for trying the app without an upload."""
25+
return pd.DataFrame(
26+
{
27+
"Customer Name": [" Ava Chen ", "Noah Patel", "Mia Lopez", "Noah Patel"],
28+
"Orders": ["3", "N/A", "5", "N/A"],
29+
"Region": ["West", "East", "West", "East"],
30+
}
31+
)
32+
33+
34+
def load_uploaded_csv(contents: bytes) -> pd.DataFrame:
35+
"""Read uploaded CSV bytes into a DataFrame."""
36+
return pd.read_csv(BytesIO(contents))
37+
38+
39+
def count_modified_columns(report: Any) -> int:
40+
"""Count columns modified by applied cleaning actions, imputations, or drops."""
41+
modified = set(report.columns_imputed) | set(report.columns_dropped)
42+
modified.update(
43+
action.column
44+
for action in report.actions
45+
if action.column is not None
46+
and action.count > 0
47+
and getattr(action, "status", "automatic") in {"automatic", "approved"}
48+
)
49+
return len(modified)
50+
51+
52+
def main() -> None:
53+
st.set_page_config(page_title="FreshData Cleaning Lab", layout="wide")
54+
st.title("FreshData Cleaning Lab")
55+
st.caption(
56+
"Upload a CSV to compare its original data with FreshData's cleaned output."
57+
)
58+
59+
uploaded_file = st.file_uploader("Choose a CSV file", type=["csv"])
60+
61+
if uploaded_file is None:
62+
raw_df = demo_data()
63+
st.info("Showing sample data. Upload a CSV to clean your own dataset.")
64+
else:
65+
try:
66+
raw_df = load_uploaded_csv(uploaded_file.getvalue())
67+
except (ValueError, UnicodeDecodeError, pd.errors.ParserError) as exc:
68+
st.error(f"Could not read this CSV: {exc}")
69+
st.stop()
70+
71+
st.sidebar.header("Cleaning Settings")
72+
strategy = st.sidebar.selectbox(
73+
"Strategy", ["balanced", "conservative", "aggressive"], index=0
74+
)
75+
drop_duplicates = st.sidebar.checkbox("Drop duplicate rows", value=True)
76+
77+
try:
78+
cleaned_df, report = fd.clean(
79+
raw_df,
80+
strategy=strategy,
81+
drop_duplicates=drop_duplicates,
82+
return_report=True,
83+
)
84+
except Exception as exc:
85+
st.error(f"FreshData could not clean this dataset: {exc}")
86+
st.stop()
87+
88+
st.subheader("Cleaning summary")
89+
metric_columns = st.columns(4)
90+
metric_columns[0].metric(
91+
"Rows removed", max(report.rows_before - report.rows_after, 0)
92+
)
93+
metric_columns[1].metric("Columns modified", count_modified_columns(report))
94+
imputed_values = sum(
95+
action.count
96+
for action in report.actions
97+
if (
98+
action.step == "impute"
99+
or (
100+
action.step == "missing"
101+
and action.column in report.columns_imputed
102+
)
103+
)
104+
and getattr(action, "status", "automatic") in {"automatic", "approved"}
105+
and action.count > 0
106+
)
107+
metric_columns[2].metric("Missing values imputed", imputed_values)
108+
metric_columns[3].metric("Duplicates dropped", report.duplicates_removed)
109+
110+
raw_column, cleaned_column = st.columns(2)
111+
with raw_column:
112+
st.subheader("Raw data")
113+
st.dataframe(raw_df, use_container_width=True)
114+
with cleaned_column:
115+
st.subheader("Cleaned data")
116+
st.dataframe(cleaned_df, use_container_width=True)
117+
118+
st.download_button(
119+
"Download cleaned CSV",
120+
data=cleaned_df.to_csv(index=False).encode("utf-8"),
121+
file_name="cleaned_data.csv",
122+
mime="text/csv",
123+
)
124+
125+
with st.expander("Quality report"):
126+
st.code(report.summary(), language="text")
127+
if report.actions:
128+
st.dataframe(
129+
pd.DataFrame(
130+
[
131+
{
132+
"Step": action.step,
133+
"Column": action.column or "",
134+
"Change": action.description,
135+
"Count": action.count,
136+
}
137+
for action in report.actions
138+
]
139+
),
140+
use_container_width=True,
141+
)
142+
143+
144+
if __name__ == "__main__":
145+
main()

0 commit comments

Comments
 (0)