Public
Edited
Dec 7, 2024
1 fork
Insert cell
Insert cell
Insert cell
import {Plot} from "@observablehq/plot"
Insert cell
import {Inputs} from "@observablehq/inputs"

Insert cell

data = FileAttachment("customer_churn_data (4).csv").csv()

Insert cell
Insert cell
Insert cell
{
// Define Dropdown Filter for Tenure Groups
const tenureGroups = ["0-12", "13-24", "25-36", "37-48", "49-60", "61-72"];
const tenureFilter = Inputs.select(tenureGroups, {
label: "Select Tenure Range"
});

// Add the Dropdown Filter to the Output
const container = html`<div></div>`;
container.appendChild(tenureFilter);

// Map Data to Tenure Groups
const groupedData = data.map(d => {
const tenure = +d.Tenure; // Ensure Tenure is numeric
let group = "0-12";
if (tenure > 12 && tenure <= 24) group = "13-24";
else if (tenure > 24 && tenure <= 36) group = "25-36";
else if (tenure > 36 && tenure <= 48) group = "37-48";
else if (tenure > 48 && tenure <= 60) group = "49-60";
else if (tenure > 60) group = "61-72";
return { ...d, TenureGroup: group };
});

// Dynamically Filter Data Based on Selected Tenure Group
tenureFilter.oninput = () => {
const filteredData = groupedData.filter(
d => d.TenureGroup === tenureFilter.value
);

// Clear previous content
container.innerHTML = '';
container.appendChild(tenureFilter);

// If No Data Matches the Selected Tenure Group
if (filteredData.length === 0) {
container.appendChild(
md`<p style="color: red;">No data available for the selected tenure group. Please check your dataset or filter conditions.</p>`
);
return;
}

// Aggregate Churn Data
const aggregatedData = filteredData.reduce((acc, d) => {
const key = d.ContractType + "-" + d.InternetService;
if (!acc[key]) {
acc[key] = { ContractType: d.ContractType, InternetService: d.InternetService, ChurnCount: 0, TotalCount: 0 };
}
acc[key].ChurnCount += d.Churn === "Yes" ? 1 : 0;
acc[key].TotalCount++;
return acc;
}, {});

const plotData = Object.values(aggregatedData).map(d => ({
ContractType: d.ContractType,
InternetService: d.InternetService,
ChurnRate: d.ChurnCount / d.TotalCount
}));

// Plot the Stacked Bar Chart
const plot = Plot.plot({
marks: [
Plot.barY(plotData, {
x: "ContractType",
y: "ChurnRate",
fill: "InternetService",
title: d =>
`Contract: ${d.ContractType}\nService: ${d.InternetService}\nChurn Rate: ${(d.ChurnRate * 100).toFixed(2)}%`
})
],
x: { label: "Contract Type" },
y: { label: "Churn Rate (Proportion of Yes)" },
color: { legend: true, label: "Internet Service Type" },
width: 800,
height: 500
});

container.appendChild(plot);
};

// Trigger the first update
tenureFilter.oninput();

return container;
}

Insert cell
Insert cell
{
// Define Filters
const minTenureFilter = Inputs.range([0, 72], {
step: 1,
value: 0,
label: "Minimum Tenure (Months)"
});

const maxTenureFilter = Inputs.range([0, 72], {
step: 1,
value: 72,
label: "Maximum Tenure (Months)"
});

const contractTypeFilter = Inputs.select(["All", "Month-to-Month", "One-Year", "Two-Year"], {
label: "Contract Type",
value: "All"
});

const internetServiceFilter = Inputs.select(["All", "DSL", "Fiber optic", "No"], {
label: "Internet Service",
value: "All"
});

// Add Filters to the Output
const container = html`<div></div>`;
container.appendChild(minTenureFilter);
container.appendChild(maxTenureFilter);
container.appendChild(contractTypeFilter);
container.appendChild(internetServiceFilter);

// Dynamically Filter Data
const updateChart = () => {
if (minTenureFilter.value > maxTenureFilter.value) {
maxTenureFilter.value = minTenureFilter.value;
}

const minTenure = minTenureFilter.value;
const maxTenure = maxTenureFilter.value;
const selectedContractType = contractTypeFilter.value;
const selectedInternetService = internetServiceFilter.value;

const filteredData = data.filter(d => {
const withinTenure = d.Tenure >= minTenure && d.Tenure <= maxTenure;
const matchesContractType =
selectedContractType === "All" || d.ContractType === selectedContractType;
const matchesInternetService =
selectedInternetService === "All" || d.InternetService === selectedInternetService;
return withinTenure && matchesContractType && matchesInternetService;
});

container.innerHTML = '';
container.appendChild(minTenureFilter);
container.appendChild(maxTenureFilter);
container.appendChild(contractTypeFilter);
container.appendChild(internetServiceFilter);

// If No Data Matches the Filters
if (filteredData.length === 0) {
container.appendChild(md`No data available for the selected filters.`);
return;
}

// Visualization: Scatter Plot
const scatterPlot = Plot.plot({
marks: [
Plot.dot(filteredData, {
x: "MonthlyCharges",
y: "Tenure",
fill: "Churn",
r: 5,
title: d =>
`Customer ID: ${d.CustomerID}\nTenure: ${d.Tenure} months\nMonthly Charges: $${d.MonthlyCharges}\nChurn: ${d.Churn}\nContract Type: ${d.ContractType}\nInternet Service: ${d.InternetService}`
})
],
x: { label: "Monthly Charges ($)" },
y: { label: "Tenure (Months)" },
color: { legend: true, label: "Churn Status" },
width: 800,
height: 600
});

container.appendChild(scatterPlot);
};

// Trigger Updates on Filter Changes
minTenureFilter.oninput = updateChart;
maxTenureFilter.oninput = updateChart;
contractTypeFilter.oninput = updateChart;
internetServiceFilter.oninput = updateChart;

// Initial Chart Update
updateChart();

return container;
}

Insert cell
Insert cell
{
// Load the data
const data = await FileAttachment("customer_churn_data (4).csv").csv();

// Process the data
const processedData = data
.filter(d => d.Age && d.MonthlyCharges && d.InternetService) // Filter out invalid rows
.map(d => ({
Age: +d.Age || 0, // Ensure Age is numeric
MonthlyCharges: +d.MonthlyCharges || 0, // Ensure Monthly Charges is numeric
ContractType: d.ContractType || "Unknown", // Fallback value
InternetService: d.InternetService || "None", // Fallback value
Churn: d.Churn === "Yes" ? 1 : 0 // Map churn to binary
}));

console.log("Processed Data:", processedData); // Debugging processed data

// Group data by age bins
const ageBins = d3
.bin()
.domain(d3.extent(processedData, d => d.Age))
.thresholds(10); // Divide into 10 bins

const binnedData = ageBins(processedData.map(d => d.Age)).map(bin => ({
AgeGroup: `${bin.x0}-${bin.x1}`,
ChurnRates: d3.rollups(
bin.map(index => processedData[index]),
v => d3.mean(v, d => d.Churn),
d => d.InternetService
).map(([internetType, churnRate]) => ({
InternetService: internetType,
ChurnRate: churnRate
}))
}));

// Create Bar Chart
const plot = () => {
const flattenedData = binnedData.flatMap(bin =>
bin.ChurnRates.map(cr => ({
AgeGroup: bin.AgeGroup,
InternetService: cr.InternetService,
ChurnRate: cr.ChurnRate || 0
}))
);

if (flattenedData.length === 0)
return html`<p style="color: red;">No data available for the selected range. Adjust the filters.</p>`;

return Plot.plot({
marks: [
Plot.barY(flattenedData, {
x: "AgeGroup",
y: "ChurnRate",
fill: "InternetService",
title: d =>
`Age Group: ${d.AgeGroup}\nInternet Service: ${d.InternetService}\nChurn Rate: ${(d.ChurnRate * 100).toFixed(
2
)}%`
})
],
x: { label: "Age Group (Years)" }, // Updated label for X-axis
y: { label: "Churn Rate (%)", tickFormat: d => `${(d * 100).toFixed(2)}%` }, // Updated label for Y-axis
color: {
legend: true, // Display legend for colors
label: "Internet Service Type",
scheme: "category10" // Consistent color scheme
},
width: 800,
height: 500
});
};

// Display Visualization with Title and Legend
return html`<h3>Churn Rate by Age Group and Internet Service Type</h3>${plot()}`;
}

Insert cell
Insert cell
{
// Load the data
const data = await FileAttachment("customer_churn_data (4).csv").csv();

// Process the data
const processedData = data
.filter(d => d.InternetService && d.TechSupport && d.Churn) // Ensure valid data
.map(d => ({
InternetService: d.InternetService || "None",
TechSupport: d.TechSupport || "No",
Churn: d.Churn === "Yes" ? 1 : 0
}));

// Filters
const internetServiceFilter = Inputs.select(
["All", ...new Set(processedData.map(d => d.InternetService))],
{ label: "Select Internet Service Type", value: "All" }
);

const techSupportFilter = Inputs.select(
["All", ...new Set(processedData.map(d => d.TechSupport))],
{ label: "Select Technical Support", value: "All" }
);

// Container for the visualization
const container = html`<div></div>`;
container.appendChild(internetServiceFilter);
container.appendChild(techSupportFilter);

const renderChart = () => {
// Filter the data
const filteredData = processedData.filter(d => {
const matchesInternetService =
internetServiceFilter.value === "All" || d.InternetService === internetServiceFilter.value;
const matchesTechSupport =
techSupportFilter.value === "All" || d.TechSupport === techSupportFilter.value;
return matchesInternetService && matchesTechSupport;
});

// Aggregate churn counts by Internet Service and Technical Support
const aggregatedData = d3.rollups(
filteredData,
group => group.reduce((sum, d) => sum + d.Churn, 0),
d => `${d.InternetService} - Tech Support: ${d.TechSupport}`
).map(([label, count]) => ({
label,
value: count
}));

// Clear the container
container.innerHTML = "";
container.appendChild(internetServiceFilter);
container.appendChild(techSupportFilter);

// If no data is available
if (aggregatedData.length === 0 || aggregatedData.every(d => d.value === 0)) {
container.appendChild(
md`**No data available for the selected filters. Adjust the filters.**`
);
return;
}

// Set up D3 Pie Chart
const width = 800;
const height = 600;
const radius = Math.min(width, height) / 2;

const svg = d3
.create("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.style("font", "12px sans-serif");

const pie = d3.pie().value(d => d.value)(aggregatedData);
const arc = d3.arc().innerRadius(50).outerRadius(radius - 10);

const color = d3.scaleOrdinal(d3.schemeCategory10);

const g = svg
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);

g.selectAll("path")
.data(pie)
.join("path")
.attr("d", arc)
.attr("fill", d => color(d.data.label))
.append("title")
.text(d => `${d.data.label}: ${d.data.value}`);

g.selectAll("text")
.data(pie)
.join("text")
.attr("transform", d => `translate(${arc.centroid(d)})`)
.attr("text-anchor", "middle")
.text(d => d.data.label);

container.appendChild(svg.node());
};

// Trigger updates on filter changes
internetServiceFilter.oninput = renderChart;
techSupportFilter.oninput = renderChart;

// Render the initial chart
renderChart();

return container;
}

Insert cell
customer_churn_data (4).csv
X*
Y*
Color
Size
Facet X
Facet Y
Mark
Auto
Type Chart, then Shift-Enter. Ctrl-space for more options.

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more