Score Distribution Analysis¶
Analyze test score distributions using histograms and visualize with Google Sheets charts.
User Story¶
You administer a certification exam and want to understand the score distribution: - What score ranges have the most test takers? - What percentile is each score at? - How many students passed vs. failed (cutoff at 70)?
You have a Google Sheet with exam scores:
| student_id | name | score |
|---|---|---|
| 1 | Alice | 85 |
| 2 | Bob | 62 |
| 3 | Carol | 91 |
| 4 | David | 75 |
| 5 | Eve | 88 |
| 6 | Frank | 58 |
| ... | ... | ... |
You need to:
- Bin scores into ranges (50-60, 60-70, 70-80, 80-90, 90-100)
- Count students in each bin
- Calculate percentiles (relative and cumulative frequency)
- Create a visual histogram to share with stakeholders
- Identify outliers and edge cases (underflow: score < 50, overflow: score ≥ 100)
With gaslamp¶
JavaScript
function cookbook_scoreDistribution() {
// Step 1: Load and validate exam scores
const schema = {
student_id: gaslamp.FlameGuards.isNumber,
name: gaslamp.FlameGuards.isString,
score: gaslamp.FlameGuards.isNumber,
};
const sourceSheet = SpreadsheetApp.getActiveSheet();
const workbook = SpreadsheetApp.getActiveSpreadsheet();
const df = gaslamp.BareFrame.fromSheet(sourceSheet);
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);
if (failed.length > 0) {
console.error(failed.length + ' validation error(s) found:');
console.error(failed.display());
return;
}
console.log(`Loaded ${passed.length} exam scores`);
// Step 2: Create histogram using ForgeFrame
// Bin scores into 10 ranges from 0 to 100
const histogram = gaslamp.ForgeFrame.from(passed).h1('score', {
nbins: 10,
xlow: 0,
xup: 100,
});
console.log('Score distribution:');
console.log(histogram.display());
// Step 3: Write histogram to sheet for visualization
const histogramSheet = workbook.getSheetByName('Score Histogram')
?? workbook.insertSheet('Score Histogram');
histogram.toSheet(histogramSheet);
// Step 4: Create a bar chart from the histogram data
const chart = histogramSheet.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(histogramSheet.getDataRange())
.setOption('title', 'Exam Score Distribution')
.setOption('hAxis.title', 'Score Range')
.setOption('vAxis.title', 'Number of Students')
.setOption('legend.position', 'none')
.setPosition(2, 5)
.build();
histogramSheet.insertChart(chart);
// Step 5: Filter histogram to find key statistics
const passRate = histogram
.filter((row) => row.get('bin').includes('[70') || row.get('bin').includes('[80') || row.get('bin').includes('[90'));
const passCount = passRate.toRows().reduce((sum, row) => sum + row.get('count'), 0);
const passPercentage = (passCount / passed.length * 100).toFixed(1);
console.log(`Pass rate (score ≥ 70): ${passPercentage}%`);
// Step 6: Export detailed statistics
const statsData = gaslamp.BareFrame.fromArrays([
['Metric', 'Value'],
['Total Exam Takers', passed.length],
['Passing Score (≥70)', passCount],
['Pass Rate (%)', passPercentage],
['Failing Count', passed.length - passCount],
['Mean Score', (passed.toRows().reduce((sum, row) => sum + row.get('score'), 0) / passed.length).toFixed(2)],
]);
const statsSheet = workbook.getSheetByName('Exam Statistics')
?? workbook.insertSheet('Exam Statistics');
statsData.toSheet(statsSheet);
console.log('Score distribution analysis complete');
}
Key gaslamp features used:
.h1()histogram method — bin continuous scores into discrete ranges with automatic count, relative frequency, and cumulative statsForgeFrame.from()— statistical analysis wrapper that returns row-oriented results (one row per bin).toSheet()integration — histogram output writes directly to Google Sheets (no manual range/cell operations)- Google Sheets charting — use
newChart()to create visual histograms from histogram data - Post-histogram filtering — use standard
.filter()on histogram results to calculate pass rates and other derived metrics - Direct calculation — combine histogram with
.reduce()for percentile and aggregation calculations
Without gaslamp¶
JavaScript
function cookbook_scoreDistribution_vanilla() {
const sourceSheet = SpreadsheetApp.getActiveSheet();
const workbook = SpreadsheetApp.getActiveSpreadsheet();
// Step 1: Load and validate exam scores
const range = sourceSheet.getDataRange();
const values = range.getValues();
const headers = values[0];
const rows = values.slice(1);
const studentIdIdx = headers.indexOf('student_id');
const nameIdx = headers.indexOf('name');
const scoreIdx = headers.indexOf('score');
const validated = [];
for (const row of rows) {
if (typeof row[studentIdIdx] !== 'number' || typeof row[nameIdx] !== 'string' || typeof row[scoreIdx] !== 'number') {
console.warn('Type error in row:', row);
continue;
}
validated.push(row);
}
if (validated.length === 0) {
console.warn('Cannot analyze: no valid rows');
return;
}
console.log(`Loaded ${validated.length} exam scores`);
// Step 2: Manually create histogram bins
const bins = {
underflow: { count: 0, label: 'underflow' },
bin1: { count: 0, label: '[0, 10)' },
bin2: { count: 0, label: '[10, 20)' },
bin3: { count: 0, label: '[20, 30)' },
bin4: { count: 0, label: '[30, 40)' },
bin5: { count: 0, label: '[40, 50)' },
bin6: { count: 0, label: '[50, 60)' },
bin7: { count: 0, label: '[60, 70)' },
bin8: { count: 0, label: '[70, 80)' },
bin9: { count: 0, label: '[80, 90)' },
bin10: { count: 0, label: '[90, 100)' },
overflow: { count: 0, label: 'overflow' },
};
// Step 3: Count scores into bins manually
const totalCount = validated.length;
for (const row of validated) {
const score = row[scoreIdx];
if (score < 0) bins.underflow.count++;
else if (score < 10) bins.bin1.count++;
else if (score < 20) bins.bin2.count++;
else if (score < 30) bins.bin3.count++;
else if (score < 40) bins.bin4.count++;
else if (score < 50) bins.bin5.count++;
else if (score < 60) bins.bin6.count++;
else if (score < 70) bins.bin7.count++;
else if (score < 80) bins.bin8.count++;
else if (score < 90) bins.bin9.count++;
else if (score < 100) bins.bin10.count++;
else bins.overflow.count++;
}
// Step 4: Calculate relative and cumulative frequencies manually
let cumulativeCount = 0;
const histogramData = [];
const binOrder = ['underflow', 'bin1', 'bin2', 'bin3', 'bin4', 'bin5', 'bin6', 'bin7', 'bin8', 'bin9', 'bin10', 'overflow'];
for (const binKey of binOrder) {
const bin = bins[binKey];
cumulativeCount += bin.count;
const relativeFreq = (bin.count / totalCount).toFixed(4);
const cumulativeFreq = (cumulativeCount / totalCount).toFixed(4);
histogramData.push([bin.label, bin.count, relativeFreq, cumulativeFreq]);
}
console.log('Score distribution:', histogramData);
// Step 5: Write histogram to sheet manually
const histogramSheet = workbook.getSheetByName('Score Histogram')
?? workbook.insertSheet('Score Histogram');
const histogramHeaders = ['bin', 'count', 'relative', 'cumulative_relative'];
histogramSheet.getRange(1, 1, 1, histogramHeaders.length).setValues([histogramHeaders]);
histogramSheet.getRange(2, 1, histogramData.length, histogramData[0].length).setValues(histogramData);
// Step 6: Create a bar chart from the histogram data
const chart = histogramSheet.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(histogramSheet.getDataRange())
.setOption('title', 'Exam Score Distribution')
.setOption('hAxis.title', 'Score Range')
.setOption('vAxis.title', 'Number of Students')
.setOption('legend.position', 'none')
.setPosition(2, 5)
.build();
histogramSheet.insertChart(chart);
// Step 7: Calculate pass rate manually (score ≥ 70)
const passCount = bins.bin8.count + bins.bin9.count + bins.bin10.count;
const passPercentage = (passCount / totalCount * 100).toFixed(1);
console.log(`Pass rate (score ≥ 70): ${passPercentage}%`);
// Step 8: Export statistics manually
const statsSheet = workbook.getSheetByName('Exam Statistics')
?? workbook.insertSheet('Exam Statistics');
// Calculate mean score manually
let totalScore = 0;
for (const row of validated) {
totalScore += row[scoreIdx];
}
const meanScore = (totalScore / totalCount).toFixed(2);
const statsData = [
['Metric', 'Value'],
['Total Exam Takers', totalCount],
['Passing Score (≥70)', passCount],
['Pass Rate (%)', passPercentage],
['Failing Count', totalCount - passCount],
['Mean Score', meanScore],
];
statsSheet.getRange(1, 1, 1, 2).setValues([['Metric', 'Value']]);
statsSheet.getRange(2, 1, statsData.length - 1, 2).setValues(statsData.slice(1));
console.log('Score distribution analysis complete');
}
Without gaslamp, you need to:
- Manually define all bin ranges (12 bins × multiple conditions = complex conditional logic)
- Manually count scores into each bin with nested if-else statements
- Manually calculate relative frequency and cumulative frequency for each bin
- Track bin order manually to preserve insertion order
- Manually construct header row with bin labels and frequency columns
- Manually write histogram data to sheet row by row with explicit range dimensions
- Manually filter and sum counts to calculate pass rate
- Manually compute mean score with a separate reduction loop
- No automatic insights — all statistics must be hand-calculated
Key Techniques¶
- ForgeFrame for histograms: Use
.h1()to create 1D histograms with automatic statistics (counts, frequencies, cumulative) - Row-oriented output: Histogram results are
BareFramewith one row per bin—ready for sheets and charts - Direct sheet integration:
.toSheet()writes histogram ready for charting (no manual cell filling) - Google Sheets Charts API: Use
newChart()with histogram data for visual distribution analysis - Post-histogram filtering: Standard DataFrame operations work on histogram results
- Statistical aggregation: Use
.reduce()to compute derived metrics (pass rate, percentiles) from histogram data - Underflow/overflow tracking: Automatic handling of out-of-range values in dedicated bins
See ForgeFrame Guide for more histogram patterns, 2D histograms, and statistical analysis techniques.