Build a Subscriber Health Dashboard
Build a Streamlit dashboard to segment subscribers for re-engagement.
Introduction
30 Second Summary
Most newsletter reports celebrate audience size. That headline number can hide subscribers who stopped clicking long before they unsubscribed.
In this project, you will build a local subscriber health dashboard with Python, pandas, and Streamlit. Your dashboard will classify anonymous activity into five clear segments for retention decisions.
What You'll Build
You'll open a browser dashboard where each acquisition-source filter updates the health metrics, segment chart, priority recommendation, and downloadable re-engagement list.
By the end of this project, you'll have:
- A subscriber health dashboard that classifies 24 anonymous records as New, Engaged, Cooling, Dormant, or Unsubscribed.
- An interactive audience filter that reveals how subscriber health changes across Website, Referral, LinkedIn, and Partner sources.
- A re-engagement handoff containing 10 candidate rows in a downloadable CSV file, supported by a Website priority recommendation.
- Secret Mission: Add a slider that lets stakeholders test different dormancy definitions without editing the code.
Are there any prerequisites?
You'll need Visual Studio Code on Windows plus basic Python familiarity. Step 1 covers the compatible Python version plus every project dependency.
Before We Start
First, let’s connect your local subscriber-health dashboard to the newsletter decision it supports. This checkpoint locks in what you are building before you start the hands-on work.
Set Up the Local Dashboard Environment
Your dashboard depends on specific Python packages. A clean local environment keeps those packages separate from your other projects.
Visual Studio Code gives you one place to manage the files and terminal. You will confirm a compatible Python version before installing pandas and Streamlit inside a virtual environment.
The final browser launch proves the setup works before you begin the subscriber analysis.
In this step, get ready to:
- Confirm a supported Python version inside the project workspace.
- Install the pinned dependencies inside an activated virtual environment.
- Launch the Streamlit Hello app in a browser.
Confirm Python and prepare the workspace
A workspace keeps every project file under one folder. Its integrated PowerShell terminal starts from that folder.
- Press the Windows key to open the search bar.
- Type Visual Studio Code and press Enter.
Why open a project folder?
Visual Studio Code treats an open folder as a workspace. The editor uses that folder as the shared location for your files and terminal.
- Click File in the top menu.
- Click Open Folder....
- Select your Desktop in the folder dialog.
- Click New Folder.
- Enter newsletter-health-dashboard as the folder name.
- Click Select Folder.
- Click Yes, I trust the authors if Workspace Trust appears.
You will see newsletter-health-dashboard at the top of the Explorer sidebar. This confirms that Visual Studio Code opened the correct workspace.
- Click Terminal in the top menu.
- Click New Terminal.
- Select PowerShell from the terminal profile menu.
PowerShell is now the active shell for your newsletter-health-dashboard folder.
Streamlit supports Python 3.11 through 3.15. Checking first prevents incompatible packages from entering the project.
- Check your installed Python version by running this command:
python --version
What does this command check?
The command prints the Python version available to the PowerShell terminal. That result determines whether your current installation can run the pinned dependencies.
✔️ My version is supported
Continue if the output shows Python 3.11 through 3.15. Your Python runtime is compatible with this project.
ⓧ My version is outside the supported range
Install Python 3.15.0 so the project uses a supported release.
- Open the official Python Windows downloads page in your browser.
- Download the current Python 3.15.0 Windows release.
- Run the downloaded installer.
- Accept the installer option that makes global Python commands available.
- Restart Visual Studio Code after the installation finishes.
- Return to the newsletter-health-dashboard workspace.
- Open a fresh PowerShell terminal.
- Confirm the new Python version by running:
python --version
What confirms the installation?
The command should now report Python 3.15.0. This version can create the project environment and install both dependencies.
ⓧ Command not found
PowerShell cannot find a Python installation. Install Python 3.15.0 before continuing.
- Open the official Python Windows downloads page in your browser.
- Download the current Python 3.15.0 Windows release.
- Run the downloaded installer.
- Accept the installer option that makes global Python commands available.
- Restart Visual Studio Code after the installation finishes.
- Return to the newsletter-health-dashboard workspace.
- Open a fresh PowerShell terminal.
- Verify that PowerShell can now find Python by running:
python --version
What confirms Python is available?
The command should print Python 3.15.0. PowerShell can now use Python from the project workspace.
Python still unavailable?
Restart Visual Studio Code so its terminal can detect the new installation. Confirm that the terminal profile is PowerShell before repeating the version check.
If the command still fails, help me make Python available in my VS Code PowerShell terminal.
Your workspace now has a compatible Python runtime. That clears the first source of package conflicts.
Build the isolated project environment
A requirements file records the exact package versions used by the dashboard. Version pins keep the project reproducible across different machines.
- Select the New File control in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the pinned project dependencies by pasting:
pandas==3.0.6
streamlit==1.66.0
What does this file control?
- The pandas==3.0.6 pin gives the project its CSV analysis tools.
- The streamlit==1.66.0 pin provides the local dashboard framework.
- Both pins make future installations use the same package versions.
- Save requirements.txt by pressing Ctrl+S.
- Confirm that requirements.txt appears in the Explorer sidebar.
Requirements file missing?
Check that the Explorer sidebar shows the newsletter-health-dashboard folder. Confirm that the file name ends with .txt.
If the file still does not appear, help me create requirements.txt in my Visual Studio Code project folder.
✔️ Awesome, I've got everything!
Great. Double-check that you saved requirements.txt before continuing.
ⓧ I'd like to double check the full code
pandas==3.0.6
streamlit==1.66.0
The .venv environment gives this project its own package directory. Activating it keeps the pinned dependencies isolated from other Python work.
- Create the virtual environment by running this command:
python -m venv .venv
What does this command create?
Python creates a .venv folder containing an isolated runtime and package location. The dashboard dependencies stay inside this project.
- Confirm that .venv appears in the Explorer sidebar.
Virtual environment missing?
Confirm that the Python version check succeeded in this PowerShell terminal. Check that requirements.txt remains visible in the same workspace.
If .venv still does not appear, help me create a Python virtual environment in my VS Code project.
- Activate the virtual environment by running:
.venv\Scripts\Activate.ps1
What does activation change?
Activation points Python commands at the runtime inside .venv. The terminal prompt also identifies the active environment.
- Confirm that the PowerShell prompt now includes (.venv).
Environment did not activate?
Confirm that the terminal profile is PowerShell. Check that the .venv folder exists in the Explorer sidebar.
If PowerShell blocks the activation script, help me activate .venv safely in PowerShell.
- Install the pinned dependencies into the active environment by running:
py -m pip install -r requirements.txt
What does this installation use?
The command asks pip to read requirements.txt. It installs the pinned pandas and Streamlit releases into the active environment.
The terminal will show download and installation progress. The command is complete when the PowerShell prompt returns without an error.
Dependency installation failed?
- Confirm that the terminal prompt includes (.venv).
- Confirm that requirements.txt contains both pinned dependency lines.
- Retry after checking that your internet connection is available.
If installation still fails, help me troubleshoot my pinned pandas and Streamlit installation.
That is the dependency layer sorted. Your dashboard now has an isolated environment with the exact packages it needs.
Launch the Streamlit Hello app
The official Hello app tests the complete local path from Python to Streamlit to your browser. A successful page load proves the environment can serve an interactive application.
Before you launch it, what do you expect your browser to show when the Streamlit server starts successfully?
- Launch the official Streamlit example by running:
python -m streamlit hello
What does this command launch?
Python runs Streamlit as a module inside the active virtual environment. Streamlit starts a local server for its official Hello app.
You will see the Streamlit Hello app open in a browser tab. The terminal remains busy while the local server is running.
Hello app did not open?
- Confirm that the terminal prompt included (.venv) before you launched the app.
- Use the local browser address printed by Streamlit if the browser did not open automatically.
- Keep the PowerShell terminal running while you check the browser.
If the page still does not load, help me troubleshoot the Streamlit Hello app.
- Return to the running PowerShell terminal.
- Press Ctrl+C to stop the Streamlit server.
The PowerShell prompt returns when the server stops. Your first browser app worked from the isolated project environment.
Your local dashboard workspace is ready. Next, you will turn synthetic subscriber activity into a visible table in the browser.
Generate and Inspect Subscriber Data
Your Visual Studio Code project has a working Streamlit environment. Now you will use pandas to turn a CSV file into your first dashboard view.
A newsletter team needs a clear way to decide which subscribers need attention. A browser table gives you a place to inspect the activity rows before making that decision.
In this step, get ready to:
- Create an anonymous subscriber dataset with 24 records.
- Build a starter app that loads the dataset into a pandas DataFrame.
- Run the app to inspect the raw subscriber table in your browser.
Create the subscriber dataset
A synthetic dataset gives you realistic newsletter activity without exposing personal information. Each row represents one anonymous subscriber.
- Select your project folder at the top of the Explorer view.
- Select the New Folder button.
You will see a folder name field inside the Explorer view.
- Type data into the folder name field.
- Press Enter.
You should see the new data folder beneath your project folder.
- Select the data folder.
- Select the New File button.
You will see a file name field beneath the data folder.
- Type subscribers.csv into the file name field.
- Press Enter.
The blank data/subscribers.csv file opens in the editor.
- Fill data/subscribers.csv with the synthetic records by pasting the dataset below:
subscriber_id,signup_date,acquisition_source,preferred_topic,emails_delivered,opens,clicks,days_subscribed,days_since_last_click,status
N001,2026-10-10,Website,Data,2,1,0,5,999,active
N002,2026-10-06,Referral,Business,3,2,1,9,3,active
N003,2026-10-12,LinkedIn,Careers,1,1,0,3,999,active
N004,2026-10-01,Partner,Technology,3,3,2,14,2,active
N005,2026-06-17,Website,Data,24,18,8,120,2,active
N006,2026-03-19,Referral,Business,30,20,6,210,7,active
N007,2026-07-17,LinkedIn,Careers,18,14,5,90,4,active
N008,2026-04-18,Partner,Technology,25,19,7,180,10,active
N009,2026-08-06,Website,Business,20,15,4,70,12,active
N010,2025-12-19,Referral,Data,32,25,9,300,1,active
N011,2026-05-08,Website,Data,20,10,3,160,18,active
N012,2026-06-27,Referral,Careers,16,9,2,110,24,active
N013,2026-03-09,Website,Business,28,14,3,220,29,active
N014,2026-07-27,LinkedIn,Technology,14,8,1,80,20,active
N015,2026-05-18,Partner,Data,22,12,2,150,26,active
N016,2026-02-17,Website,Careers,26,8,2,240,65,active
N017,2026-04-08,Referral,Business,18,6,1,190,45,active
N018,2026-03-19,Website,Technology,24,5,0,210,999,active
N019,2025-12-19,Partner,Careers,30,9,3,300,90,active
N020,2026-06-17,Website,Data,15,3,0,120,999,active
N021,2026-01-18,Website,Data,20,8,2,270,80,unsubscribed
N022,2026-02-07,Referral,Business,16,7,1,250,100,unsubscribed
N023,2026-04-18,LinkedIn,Careers,12,5,1,180,70,unsubscribed
N024,2026-05-28,Partner,Technology,10,4,0,140,999,unsubscribed
What does this data capture?
- The subscriber_id field identifies each record without using personal information.
- The signup_date field records when the subscription began.
- The acquisition_source field records where the subscriber came from.
- The preferred_topic field records the subscriber's content interest.
- The emails_delivered field records delivery volume.
- The opens field provides activity context.
- The clicks field captures a direct subscriber action.
- The days_subscribed field expresses subscription age.
- The days_since_last_click field captures click recency.
- The status field distinguishes active records from unsubscribed records.
- Save data/subscribers.csv.
- Scroll to the final record in the file.
You should see N024 as the final subscriber ID. The file now contains one header row plus 24 anonymous records.
Does the dataset look misaligned?
Check that every record occupies one line. Confirm that commas separate each value.
Make sure the file is named subscribers.csv inside the data folder.
Help me check why my subscriber CSV is not forming consistent rows.
✔️ Awesome, I've got everything!
Your subscriber file now matches the project dataset.
ⓧ I'd like to double check the full code
subscriber_id,signup_date,acquisition_source,preferred_topic,emails_delivered,opens,clicks,days_subscribed,days_since_last_click,status
N001,2026-10-10,Website,Data,2,1,0,5,999,active
N002,2026-10-06,Referral,Business,3,2,1,9,3,active
N003,2026-10-12,LinkedIn,Careers,1,1,0,3,999,active
N004,2026-10-01,Partner,Technology,3,3,2,14,2,active
N005,2026-06-17,Website,Data,24,18,8,120,2,active
N006,2026-03-19,Referral,Business,30,20,6,210,7,active
N007,2026-07-17,LinkedIn,Careers,18,14,5,90,4,active
N008,2026-04-18,Partner,Technology,25,19,7,180,10,active
N009,2026-08-06,Website,Business,20,15,4,70,12,active
N010,2025-12-19,Referral,Data,32,25,9,300,1,active
N011,2026-05-08,Website,Data,20,10,3,160,18,active
N012,2026-06-27,Referral,Careers,16,9,2,110,24,active
N013,2026-03-09,Website,Business,28,14,3,220,29,active
N014,2026-07-27,LinkedIn,Technology,14,8,1,80,20,active
N015,2026-05-18,Partner,Data,22,12,2,150,26,active
N016,2026-02-17,Website,Careers,26,8,2,240,65,active
N017,2026-04-08,Referral,Business,18,6,1,190,45,active
N018,2026-03-19,Website,Technology,24,5,0,210,999,active
N019,2025-12-19,Partner,Careers,30,9,3,300,90,active
N020,2026-06-17,Website,Data,15,3,0,120,999,active
N021,2026-01-18,Website,Data,20,8,2,270,80,unsubscribed
N022,2026-02-07,Referral,Business,16,7,1,250,100,unsubscribed
N023,2026-04-18,LinkedIn,Careers,12,5,1,180,70,unsubscribed
N024,2026-05-28,Partner,Technology,10,4,0,140,999,unsubscribed
How to compare this file
The first line defines ten columns. The remaining 24 lines run from N001 through N024.
Build the raw-data app
A DataFrame gives pandas a table-shaped structure for the subscriber records. The app loads that structure once before passing it to Streamlit for display.
- Select your project folder at the top of the Explorer view.
- Select the New File button.
You will see a file name field beneath the project folder.
- Type app.py into the file name field.
- Press Enter.
The blank app.py file opens in the editor.
- Build the starter dashboard by pasting the code below into app.py.
import pandas as pd
import streamlit as st
st.title("Newsletter Subscriber Health Dashboard")
df = pd.read_csv("data/subscribers.csv")
st.dataframe(df, hide_index=True)
What does this code do?
- The imports make pandas available as pd and Streamlit available as st.
- The st.title() call gives the browser page a clear dashboard heading.
- The pd.read_csv() call loads data/subscribers.csv into the df DataFrame.
- The st.dataframe() call displays the records as an interactive table without a separate row-number column.
- Save app.py.
You should now see app.py beside requirements.txt in the Explorer view.
✔️ Awesome, I've got everything!
Your starter app is saved with the data-loading flow in place.
ⓧ I'd like to double check the full code
import pandas as pd
import streamlit as st
st.title("Newsletter Subscriber Health Dashboard")
df = pd.read_csv("data/subscribers.csv")
st.dataframe(df, hide_index=True)
How to compare this file
The complete file contains two imports. It then adds the title before loading and displaying the dataset.
Run and inspect the raw-data app
Streamlit keeps a local server running while you use the browser. Your activated PowerShell terminal remains occupied while that server is running.
Before you run it, do you think the raw table alone will reveal a clear priority audience?
- Launch the dashboard from the activated PowerShell terminal by running:
python -m streamlit run app.py
What does this command do?
The command uses your active virtual environment to start Streamlit. Streamlit runs app.py as a local browser app.
That's your first project-specific app running in the browser. You should see the Newsletter Subscriber Health Dashboard title above an interactive table containing 24 records.
Browser table not loading?
Confirm that the terminal prompt still shows the active .venv environment.
Check that app.py sits beside the data folder in the Explorer view. Confirm that subscribers.csv is inside that folder.
Help me troubleshoot why my Streamlit app is not displaying the subscriber table.
- Select the acquisition_source column header to sort the records by source.
- Scroll through the sorted subscriber records.
You can inspect each subscriber's activity fields and compare acquisition sources. However, the raw table has no health category that identifies a priority audience for an intervention.
Your subscriber data is now visible in a working browser app. Next, you will turn those raw activity rows into subscriber-health segments a newsletter team can act on.
Turn Raw Activity into Subscriber Segments
The raw table in your Streamlit app proves that all 24 subscriber records load correctly. However, rows of activity still do not identify the audience that needs attention.
An interpretable behavioral segmentation layer turns those fields into labels a non-technical team can use. You will use pandas to classify each DataFrame row before Streamlit charts the resulting counts.
In this step, get ready to:
- Define transparent rules for five subscriber-health segments.
- Apply classification to every subscriber record.
- Display the segment counts in a bar chart.
Define the segment rules
A rule-based classifier evaluates each subscriber against a clear sequence of conditions. The first matching condition supplies the segment label.
Why rule-based segments?
Predictive churn modeling depends on validated historical outcomes. This synthetic dataset has no validated churn labels.
Transparent rules keep every assumption visible to stakeholders. The company can validate each threshold against its publishing cadence before using the dashboard for a real campaign.
- In app.py, find the line containing st.title.
- Insert the classifier directly above that line by copying the code below:
def classify_subscriber(row):
"""Assign an interpretable demonstration segment to one subscriber."""
if row["status"] == "unsubscribed":
return "Unsubscribed"
if row["days_subscribed"] <= 14:
return "New"
if row["clicks"] >= 4 and row["days_since_last_click"] <= 14:
return "Engaged"
if row["clicks"] > 0 and row["days_since_last_click"] <= 30:
return "Cooling"
return "Dormant"
How do the rules work?
The conditions run from top to bottom. This order ensures that each subscriber receives exactly one segment.
- The status check preserves every unsubscribed record as Unsubscribed.
- The days_subscribed check labels accounts that are 14 days old or younger as New.
- High recent click activity produces the Engaged label.
- Lower recent click activity produces the Cooling label.
- Every remaining active record becomes Dormant.
- Save app.py.
- Switch back to the running Streamlit browser tab.
The app should rerun successfully. You should still see the dashboard title above the table of 24 records.
Does the app stop rerunning?
Check that each if line is indented inside classify_subscriber(). Check that each return line is indented inside its matching condition.
Compare the quotation marks around every column name. A missing mark prevents Python from reading the function.
Help me fix my subscriber classifier.
Apply the rules to every subscriber
The classifier handles one subscriber row at a time. DataFrame.apply with axis=1 passes each row through the function.
- In app.py, find this existing section beneath the classifier:
st.title("Newsletter Subscriber Health Dashboard")
df = pd.read_csv("data/subscribers.csv")
What is the current data path?
The title renders first. The CSV then loads directly into df without a segment column.
- Replace that section with the expanded version below:
st.title("Newsletter Subscriber Health Dashboard")
st.write(
"This prototype uses synthetic data. Its 14-day and 30-day rules are "
"demonstration assumptions that should be validated against company cadence."
)
df = pd.read_csv("data/subscribers.csv")
df["segment"] = df.apply(classify_subscriber, axis=1)
What does this change do?
- The st.write message tells stakeholders that the thresholds are demonstration assumptions.
- The row-wise df.apply call runs classify_subscriber once for every record.
- Each returned label becomes a value in the new segment column.
- Predict which segment subscriber N001 will receive from its subscription age.
- Save app.py.
- Switch back to the running Streamlit browser tab.
You should see the assumptions note above the table. You should also see a new segment column containing all five labels.
Is the segment column missing?
Make sure the df["segment"] assignment appears after pd.read_csv. The DataFrame must exist before pandas can classify its rows.
Confirm that the function name is spelled classify_subscriber. Confirm that the apply call includes axis=1.
Help me find my missing segment column.
Count and chart the segments
A bar chart needs one count for each segment. The count_values() helper summarizes the labels before chart_data reshapes them for the chart.
- In app.py, find the end of classify_subscriber().
- Insert the counting helper beneath the classifier by copying the code below:
def count_values(values):
"""Count values without adding another dependency."""
counts = {}
for value in values:
counts[value] = counts.get(value, 0) + 1
return counts
How does the counter work?
The counts dictionary stores one total for each segment label. Each loop increases the matching total by one.
The completed dictionary supplies the segment names and subscriber totals required by the chart.
- Save app.py.
- Switch back to the running Streamlit browser tab.
The app should rerun successfully. The segmented table should remain visible with all 24 records.
- In app.py, find these lines near the bottom of the file:
df["segment"] = df.apply(classify_subscriber, axis=1)
st.dataframe(df, hide_index=True)
Where will the chart go?
The segment column is ready at this point. The existing table renders immediately beneath it.
The chart belongs between these lines so stakeholders see the summary before the individual records.
- Replace those lines with the completed chart section below:
df["segment"] = df.apply(classify_subscriber, axis=1)
segment_counts = count_values(df["segment"])
chart_data = pd.DataFrame(
{
"segment": list(segment_counts.keys()),
"subscribers": list(segment_counts.values()),
}
)
st.write("### Segment distribution")
st.bar_chart(chart_data, x="segment", y="subscribers")
st.dataframe(df, hide_index=True)
What builds the chart?
- The segment_counts dictionary stores the total for each label.
- The chart_data DataFrame pairs each segment with its subscriber count.
- The st.bar_chart call uses those two columns as the chart axes.
- The final st.dataframe call keeps the segmented records available beneath the summary.
- Save app.py.
- Switch back to the running Streamlit browser tab.
You should see a segment distribution bar chart above the table. The table should still contain all 24 records.
✔️ Awesome, I've got everything!
Your incremental edits match the target structure. app.py is ready for final verification.
ⓧ I'd like to double check the full code
import pandas as pd
import streamlit as st
def classify_subscriber(row):
"""Assign an interpretable demonstration segment to one subscriber."""
if row["status"] == "unsubscribed":
return "Unsubscribed"
if row["days_subscribed"] <= 14:
return "New"
if row["clicks"] >= 4 and row["days_since_last_click"] <= 14:
return "Engaged"
if row["clicks"] > 0 and row["days_since_last_click"] <= 30:
return "Cooling"
return "Dormant"
def count_values(values):
"""Count values without adding another dependency."""
counts = {}
for value in values:
counts[value] = counts.get(value, 0) + 1
return counts
st.title("Newsletter Subscriber Health Dashboard")
st.write(
"This prototype uses synthetic data. Its 14-day and 30-day rules are "
"demonstration assumptions that should be validated against company cadence."
)
df = pd.read_csv("data/subscribers.csv")
df["segment"] = df.apply(classify_subscriber, axis=1)
segment_counts = count_values(df["segment"])
chart_data = pd.DataFrame(
{
"segment": list(segment_counts.keys()),
"subscribers": list(segment_counts.values()),
}
)
st.write("### Segment distribution")
st.bar_chart(chart_data, x="segment", y="subscribers")
st.dataframe(df, hide_index=True)
What should match?
Compare the function names and rule order first. Then compare the assumption note plus the chart section near the bottom of the file.
- Switch back to the PowerShell terminal from earlier.
- Stop the current Streamlit server by pressing Ctrl+C.
- Predict whether relaunching the app will reproduce all five segment counts.
- Relaunch the dashboard by running this command:
python -m streamlit run app.py
What does this command verify?
This command starts the dashboard from the completed app.py file. A successful launch proves that the classifier and chart code work together.
- Switch to the browser tab opened by Streamlit.
You should see 4 New records, 6 Engaged records, 5 Cooling records, 5 Dormant records, and 4 Unsubscribed records. That is the segmentation layer working across all 24 subscribers.
Do the chart counts differ?
Check that the rules appear in the same order as the completed code. An earlier broad condition can capture a record before it reaches the intended segment.
Confirm that the New threshold is 14 days. Confirm that the Cooling threshold is 30 days.
Help me diagnose different segment counts.
Your dashboard can now turn raw activity into a clear audience-health summary. Next, you will add decision metrics plus the export that carries re-engagement candidates into an experiment.
Build the Decision Dashboard and Export
Your Streamlit dashboard now classifies 24 synthetic records into five subscriber-health segments. The segment chart proves that your rules work across the full audience.
Those segments become useful when a stakeholder can narrow the audience. The dashboard also needs to turn the selected records into a handoff for an experiment.
In this step, get ready to:
- Build an acquisition-source filter with audience metrics.
- Identify the highest-priority acquisition source.
- Export the re-engagement candidates as a CSV file.
Filter the audience and add decision metrics
An acquisition-source filter creates a smaller DataFrame for each stakeholder question. Every metric below the filter can then describe the selected audience.
- In app.py, find df["segment"] = df.apply(classify_subscriber, axis=1).
- Paste the following block directly below that line:
source_options = ["All"] + sorted(set(df["acquisition_source"]))
selected_source = st.selectbox("Acquisition source", source_options)
if selected_source == "All":
filtered = df
else:
filtered = df[df["acquisition_source"] == selected_source]
What does this filter do?
- The source_options list combines All with every acquisition source in the dataset.
- The selected_source variable stores the current selectbox choice.
- The conditional creates filtered from the records that match that choice.
- Save app.py.
- Switch back to the dashboard in your browser.
Streamlit reruns the file automatically. You should see an Acquisition source selectbox containing All, LinkedIn, Partner, Referral, and Website.
Selectbox missing?
- Confirm that the new block sits below the line that creates the segment column.
- Check that source_options uses the existing acquisition_source column name.
Ask for help with the exact placement by selecting help me find why my Streamlit acquisition-source selectbox is missing.
The selected audience now needs a measurable opportunity size. In this project, re-engagement candidates are the records classified as Cooling or Dormant.
- Find segment_counts = count_values(df["segment"]) in app.py.
- Replace that line with the following block:
reengagement = filtered[filtered["segment"].isin(["Cooling", "Dormant"])]
st.metric("Subscriber records", len(filtered))
st.metric("Engaged subscribers", sum(filtered["segment"] == "Engaged"))
st.metric("Re-engagement candidates", len(reengagement))
segment_counts = count_values(filtered["segment"])
How do the metrics support a decision?
- The reengagement DataFrame keeps the selected Cooling and Dormant records.
- The first metric shows the size of the filtered audience.
- The second metric counts filtered Engaged subscribers.
- The third metric measures the re-engagement opportunity.
- The updated segment_counts makes the chart respond to the same filter.
- Save app.py.
- Switch back to the dashboard in your browser.
- Select All in the Acquisition source selectbox.
You should see 24 subscriber records. You should also see 6 Engaged subscribers plus 10 re-engagement candidates.
- Select Referral in the Acquisition source selectbox.
You should see 6 subscriber records. The chart should update to the segments found within that source.
Metrics not changing?
- Confirm that each metric reads from filtered or reengagement.
- Confirm that segment_counts reads from filtered["segment"].
Use help me debug why my Streamlit metrics do not respond to selected_source if the full audience still appears.
Recommend an audience and focus the table
Candidate counts show the size of the opportunity. A source recommendation gives the team a concrete place to begin the re-engagement experiment.
- In app.py, find st.bar_chart(chart_data, x="segment", y="subscribers").
- Paste the following block directly below that line:
source_counts = count_values(reengagement["acquisition_source"])
if source_counts:
priority_source = max(source_counts, key=source_counts.get)
st.write(
f"### Recommendation\nStart the re-engagement experiment with the "
f"{priority_source} audience, which contributes "
f"{source_counts[priority_source]} current candidates."
)
else:
st.write("### Recommendation\nNo re-engagement candidates match this filter.")
How is the recommendation chosen?
- The source_counts dictionary counts each acquisition source within the candidate list.
- The max() call finds the source with the largest candidate count.
- The fallback message handles a filter with no current candidates.
- Save app.py.
- Switch back to the dashboard in your browser.
- Select All in the Acquisition source selectbox.
You should see a recommendation to start with the Website audience. The message should show that Website contributes 5 current candidates.
Recommendation missing?
- Confirm that source_counts reads from reengagement["acquisition_source"].
- Check that the recommendation block sits below the segment chart.
Select help me debug why source_counts is not producing a Website recommendation for targeted help.
A recommendation should stay auditable through the underlying records. A focused details table gives stakeholders the fields needed to inspect each classification.
- Find st.dataframe(df, hide_index=True) near the bottom of app.py.
- Replace that line with the following block:
st.write("### Subscriber details")
display_columns = [
"subscriber_id",
"acquisition_source",
"preferred_topic",
"emails_delivered",
"opens",
"clicks",
"days_since_last_click",
"status",
"segment",
]
st.dataframe(filtered[display_columns], hide_index=True)
Why select these columns?
- The identity fields show which anonymous subscriber record the team is reviewing.
- The source plus topic fields provide experiment context.
- The activity fields show the evidence behind each segment.
- The filtered DataFrame keeps the table aligned with the current selectbox choice.
- Save app.py.
- Switch back to the dashboard in your browser.
- Select LinkedIn in the Acquisition source selectbox.
You should see 4 rows in the Subscriber details table. Each row should include its source plus topic plus activity plus status plus segment fields.
Table showing every source?
- Confirm that the final table uses filtered[display_columns].
- Check every name in display_columns against the CSV header.
Choose help me fix a Streamlit details table that ignores selected_source if the table still shows all 24 records.
Export the re-engagement audience
The candidate list becomes actionable when another team can take it into an experiment. DataFrame.to_csv(index=False) creates the handoff without adding an extra index column.
- Find st.dataframe(filtered[display_columns], hide_index=True) at the bottom of app.py.
- Paste the following block directly below that line:
st.download_button(
label="Download re-engagement candidates",
data=reengagement.to_csv(index=False),
file_name="reengagement_candidates.csv",
mime="text/csv",
)
What does the download code do?
- The data argument converts the filtered candidate records into CSV text.
- The file_name argument gives the downloaded handoff a consistent name.
- The mime argument identifies the download as CSV content.
- Save app.py.
✔️ Awesome, I've got everything
Your decision dashboard code is complete. Double-check that app.py is saved.
ⓧ I'd like to double check the full code
import pandas as pd
import streamlit as st
def classify_subscriber(row):
"""Assign an interpretable demonstration segment to one subscriber."""
if row["status"] == "unsubscribed":
return "Unsubscribed"
if row["days_subscribed"] <= 14:
return "New"
if row["clicks"] >= 4 and row["days_since_last_click"] <= 14:
return "Engaged"
if row["clicks"] > 0 and row["days_since_last_click"] <= 30:
return "Cooling"
return "Dormant"
def count_values(values):
"""Count values without adding another dependency."""
counts = {}
for value in values:
counts[value] = counts.get(value, 0) + 1
return counts
st.title("Newsletter Subscriber Health Dashboard")
st.write(
"This prototype uses synthetic data. Its 14-day and 30-day rules are "
"demonstration assumptions that should be validated against company cadence."
)
df = pd.read_csv("data/subscribers.csv")
df["segment"] = df.apply(classify_subscriber, axis=1)
source_options = ["All"] + sorted(set(df["acquisition_source"]))
selected_source = st.selectbox("Acquisition source", source_options)
if selected_source == "All":
filtered = df
else:
filtered = df[df["acquisition_source"] == selected_source]
reengagement = filtered[filtered["segment"].isin(["Cooling", "Dormant"])]
st.metric("Subscriber records", len(filtered))
st.metric("Engaged subscribers", sum(filtered["segment"] == "Engaged"))
st.metric("Re-engagement candidates", len(reengagement))
segment_counts = count_values(filtered["segment"])
chart_data = pd.DataFrame(
{
"segment": list(segment_counts.keys()),
"subscribers": list(segment_counts.values()),
}
)
st.write("### Segment distribution")
st.bar_chart(chart_data, x="segment", y="subscribers")
source_counts = count_values(reengagement["acquisition_source"])
if source_counts:
priority_source = max(source_counts, key=source_counts.get)
st.write(
f"### Recommendation\nStart the re-engagement experiment with the "
f"{priority_source} audience, which contributes "
f"{source_counts[priority_source]} current candidates."
)
else:
st.write("### Recommendation\nNo re-engagement candidates match this filter.")
st.write("### Subscriber details")
display_columns = [
"subscriber_id",
"acquisition_source",
"preferred_topic",
"emails_delivered",
"opens",
"clicks",
"days_since_last_click",
"status",
"segment",
]
st.dataframe(filtered[display_columns], hide_index=True)
st.download_button(
label="Download re-engagement candidates",
data=reengagement.to_csv(index=False),
file_name="reengagement_candidates.csv",
mime="text/csv",
)
What should this file contain?
This file connects classification to filtering. It then turns the filtered records into metrics plus a recommendation plus an export.
- Return to the PowerShell terminal from earlier.
- Stop the running Streamlit server by pressing Ctrl+C.
Before the final run, consider whether the completed dashboard keeps all 24 records while narrowing the re-engagement audience.
- Start the completed dashboard by running this command:
python -m streamlit run app.py
What does this command do?
The command runs app.py through Streamlit. It serves the completed dashboard in your browser.
Your browser should open the completed Newsletter Subscriber Health Dashboard. The acquisition-source selectbox should appear above the three metric cards.
- Select All in the Acquisition source selectbox.
- Read the three metric cards.
- Confirm that the recommendation names Website.
You should see 24 subscriber records plus 6 Engaged subscribers plus 10 re-engagement candidates. The recommendation should identify Website as the priority source.
- Click Download re-engagement candidates.
- Open the downloaded reengagement_candidates.csv file from your browser's download area.
You should see 10 candidate data rows beneath the header. Every row should belong to the Cooling or Dormant segment.
Download missing or empty?
- Confirm that the download button sits below the details table in app.py.
- Check that the data argument uses reengagement.to_csv(index=False).
- Select All before downloading the 10-row verification file.
Use help me troubleshoot an empty reengagement_candidates.csv download if the candidate rows are still missing.
That completes the decision loop. Your dashboard now turns subscriber behavior into a downloadable experiment audience.
Secret mission
Make the Dormancy Rule Interactive
A fixed 30-day dormancy rule cannot reflect every newsletter cadence. Add a slider that lets stakeholders test thresholds from 21 to 90 days. Watch every dashboard output update from the same assumption.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep the dashboard running or pause it for later. You can also delete its local files without any cloud cleanup or ongoing costs.
Resources you used:
- The local Streamlit server that powers your browser dashboard.
- The active .venv virtual environment in PowerShell.
- The project folder open in VS Code, including app.py, requirements.txt, data/subscribers.csv, and .venv.
- Any downloaded CSV copies named reengagement_candidates.csv.
Keep everything running
No action is needed. Choose this option while you are demonstrating the dashboard or continuing to refine it.
- Leave the Streamlit terminal running while you demonstrate the dashboard.
- Keep the .venv active in PowerShell.
- Keep the VS Code project open for future edits.
- Retain any downloaded candidate files for your re-engagement proposal.
Pause - I'll come back to this later
Stop the local process to free up system resources. Your code and synthetic subscriber data remain available for your return.
- Stop the Streamlit server by pressing Ctrl+C in the PowerShell terminal.
- Exit the active virtual environment by running deactivate in the same terminal.
- Keep the project folder open in VS Code for your next session.
Your dashboard is safely paused. The PowerShell prompt returns without the .venv prefix.
Delete - I don't want to use this again
Remove every local artifact when you want a fresh start. No cloud cleanup is required.
Deletion Is Permanent
Deleting the project folder removes the dashboard code and its synthetic data. You need another copy of these files to restore this exact dashboard later.
Delete the project folder:
- Stop the Streamlit server by pressing Ctrl+C in the PowerShell terminal.
- Exit the active virtual environment by running deactivate in the same terminal.
- Right-click the top-level project folder in the VS Code Explorer sidebar.
- Choose the option that reveals this folder in File Explorer.
- Close VS Code.
- Move up one folder level in File Explorer.
- Select the project folder that you just revealed.
- Press Delete to remove the selected folder.
The project folder should no longer appear at its previous location.
Delete downloaded candidate files:
- Enter reengagement_candidates.csv in the File Explorer search field.
- Delete every downloaded copy shown in the search results.
Cleanup complete. Your machine no longer holds the project folder or its downloaded candidate list.
Nice Work!
Nice Work!
That is a strong finish. Your local newsletter subscriber health dashboard now turns synthetic activity data into a clear retention decision.
You've learned how to:
- Use pandas to classify 24 anonymous records into five behavioral segments with transparent click and recency rules.
- Build an interactive Streamlit decision dashboard for comparing subscriber health across acquisition sources.
- Export 10 re-engagement candidates to reengagement_candidates.csv for a proposed retention experiment.
- Secret Mission: Make the dormancy definition interactive so stakeholders can test different newsletter cadences.
Ready to quiz yourself?