What I Learned Studying a Python Sales Analytics Dashboard (Streamlit + Pandas)
A quick note before anything else. I did not build this dashboard. It was created by Asha Kalavagunta, and her original repo is here: github.com/Ashakalavagunta/sales-analytics-dashboard. I have a copy on my GitHub, and I spent time reading through the code to understand how a complete data project fits together.
This post is what I took away from that. If you are a student who knows some Python and wants to see what a real Pandas and Streamlit project looks like end to end, this is a good one to study.
What the project is
It is a sales dashboard built on the Kaggle Sample Superstore dataset. The data has 9,994 rows and 21 raw columns, covering January 2014 to December 2017 across four US regions. After cleaning, the table has 30 columns.
The stack is simple:
- Python 3.10+
- Pandas and NumPy for the data work
- Plotly Express and Graph Objects for charts
- Streamlit for the web interface
- Streamlit Community Cloud for hosting
The repo is small. There are four files that matter: app.py, etl_pipeline.py, cleaned_data.csv and requirements.txt.
The structure: two scripts, one clean file
The first thing I liked is the separation. The cleaning logic lives in etl_pipeline.py. It reads the raw CSV and writes cleaned_data.csv. Then app.py reads that cleaned file and draws the dashboard.
So the flow is:
- Raw Superstore CSV
etl_pipeline.pycleans it and adds new columnscleaned_data.csvis savedapp.pyloads it, applies filters, shows KPIs and charts
Notice that app.py is not fragile about this. Its load_data() function tries cleaned_data.csv first, then falls back to the raw Sample - Superstore.csv. It is wrapped in @st.cache_data(ttl=300), so Streamlit does not re-read the file on every click. That one decorator is worth remembering. Streamlit reruns the whole script on every interaction, and caching is how you keep that fast.
The ETL step, in plain words
ETL means extract, transform, load. The script literally labels its sections that way. Here is what happens.
Extract. The CSV is loaded with latin1 encoding:
df = pd.read_csv(RAW_FILE, encoding='latin1')
The Superstore file has characters that break the default UTF-8 reader, so this encoding argument matters. Good tip if you ever hit a decode error on a downloaded CSV.
Clean. Column names are normalised so spaces and hyphens become underscores. That makes Order Date into Order_Date, which you can access cleanly in code:
df.columns = [c.strip().replace(' ', '_').replace('-', '_') for c in df.columns]
Dates are parsed and the numeric columns (Sales, Profit, Discount, Quantity) are coerced with errors='coerce'. That setting turns anything unparseable into a null instead of crashing. After that, rows missing Sales, Profit or Order_Date are dropped, and duplicates are removed.
Transform. This is where new columns get created. More on that below.
Validate. I think this is the most underrated part. The script ends with a handful of assert checks: sales must be positive, ship days cannot be negative, no duplicates remain, no null order dates. If any of these fail, the script stops. It is a cheap way to trust your own output.
Derived columns (feature engineering)
Raw data rarely has exactly what you want to chart. The ETL script builds these from the existing columns:
Year,Month,Month_Name,QuarterandYearQuarter, all from Order_DateDays_to_Ship, which is Ship_Date minus Order_DateProfit_Margin, which is profit divided by sales, times 100Revenue_Band, which buckets each order as Low, Medium, High or Premium
Two lines show the style well:
df['Days_to_Ship'] = (df['Ship_Date'] - df['Order_Date']).dt.days.clip(lower=0)
df['Profit_Margin'] = (df['Profit'] / df['Sales'].replace(0, np.nan) * 100).round(2)
What I picked up here: the .clip(lower=0) stops negative ship days, and .replace(0, np.nan) avoids dividing by zero. Small defensive details like these are what separate a script that works once from one that keeps working.
The revenue bands use pd.cut with bins at 0, 50, 200 and 500, so orders fall into Low (under $50), Medium, High or Premium (over $500).
KPI cards
The top of the dashboard shows five numbers: Total Revenue, Net Profit, Total Orders, Unique Customers and Average Discount. They are computed straight from the filtered DataFrame. Total orders, for example, uses nunique() on Order_ID, because one order can have several rows.
That is a good Pandas lesson by itself. Counting rows and counting orders are not the same thing.
The charts
The README counts 14 charts. Reading through app.py, they cover:
- Monthly revenue and profit as a line chart
- Revenue by region as a bar chart
- Category split as a donut
- Segment bar chart
- Top sub-categories
- Profit margin by sub-category
- Ship mode as a donut
- Discount vs profit as a scatter plot
- Yearly revenue and profit as grouped bars
- A US state choropleth map
- Days to ship by mode
- A month by year heatmap
- A top 10 products table
Every one is drawn with Plotly and shown with st.plotly_chart(fig, use_container_width=True). Once you see one chart done, the others are variations. I found that reassuring. The pattern is: group the data with Pandas, pass it to Plotly, hand the figure to Streamlit.
Filters
The sidebar has six filters: year, quarter, region, segment, category and ship mode. There is also an order date range. All are multiselects, and the data is cut down with a single boolean mask:
mask = (
df['Year'].isin(sel_years) &
df['Quarter'].isin(sel_quarters) &
df['Region'].isin(sel_regions) &
df['Segment'].isin(sel_segments) &
df['Category'].isin(sel_categories) &
df['Ship_Mode'].isin(sel_ship)
)
Then fdf = df[mask].copy() becomes the DataFrame every KPI and chart reads from. So changing one filter updates the entire page. This is the core idea of the whole app, and it is just isin() joined with &. There is also a button to download the filtered data as CSV.
What I would try differently
These are my own thoughts as a learner, not criticism of Asha's work.
- Some of the insight card text in the app is typed in as fixed text rather than calculated from the data. I would try generating those lines from the filtered numbers.
- I would experiment with splitting
app.pyinto smaller files, since one script holds all the layout and charts. - The ETL logic appears twice in a similar form, once in
etl_pipeline.pyand once inload_data(). I would try to share one function.
I have not built any of these yet. They are on my list for when I start my own project.
Why this was worth studying
Tutorials show you one concept at a time. A finished project shows you how the pieces connect: where cleaning ends, where charts begin, how filters touch everything. Reading someone else's code, with credit, is a fair way to learn.
If you want to explore it, go to Asha's original repository and run the ETL script yourself. If you want to follow what I study and build next, my profile is github.com/Pranaav717.
For a gentler start on the Pandas side, see Pandas for Beginners: Cleaning and Analysing Real-World Data. For the ETL idea in more depth, see Understanding ETL in Python Through a Practical Project.
All code snippets above are from Asha Kalavagunta's project, quoted briefly for learning.
Comments
Join the discussionNo comments yet. Be the first to comment.