Concept · beginner
Descriptive statistics
Count, center, and spread of one numeric column.
Before you model data, you describe it. Descriptive statistics boil a column of numbers down to a handful of values that tell you how many there are, where they center, and how widely they spread. Every later concept on this site leans on these: regression is built from means and variances, and k-means moves centroids to means.
The dataset
data.csv holds 120 days of one person’s commute, in minutes. The data is
synthetic: we drew it once from a log-normal distribution with a fixed seed
(see make_data.py) and committed the result. Log-normal is a good fit for
commutes. Most days land near a typical time, but traffic can’t make a trip
much shorter, only much longer, so a few bad days stretch out to the right.
Here, 8 of the 120 days took longer than 45 minutes, and the worst took 61.3.
Center: mean and median
The mean is the sum divided by the count. The median is the middle value once the data is sorted, or the average of the two middle values when the count is even, as it is here.
For these commutes, the mean is 29.68 minutes and the median is 28.75. The mean is higher because the long right tail pulls it up, while the median only cares about the order of the values. To see how differently they react, imagine the worst day had taken 200 minutes instead of 61.3. The mean would rise to 30.83, but the median wouldn’t move at all. When a few extreme values matter less than the typical case, the median is usually the better summary.
Spread: range, variance, and standard deviation
The range, maximum minus minimum, is the simplest measure of spread, but it depends on only the two most extreme days.
The variance uses every value. It averages the squared distance of each value from the mean. Squaring keeps values above and below the mean from canceling out, and it weighs large deviations more than small ones. The standard deviation is the square root of the variance, which puts it back in the data’s own units: minutes, not minutes squared.
Why divide by instead of ? These 120 days are a sample of all the days this commute could take. The deviations are measured from the sample’s own mean, which by construction sits as close to these particular values as possible, so they come out slightly smaller than deviations from the true long-run mean would. Dividing by (Bessel’s correction) makes up for that. With 120 values the difference is under 1%, but with 5 it’s 25%.
Here the standard deviation is 9.14 minutes. A useful reading: a typical day lands within about 9 minutes of the average. In this data, 87 of the 120 days (72%) fall within one standard deviation of the mean.
What to watch for across languages
None of the from-scratch implementations calls a ready-made mean, median,
variance, or standard deviation function, even where the language has one.
Python, R, Julia, and PostgreSQL do have them, and their tabs have a
Built-in switch that shows the short version. All four divide by n − 1,
but the names differ: variance in Python, var in R and Julia, and
var_samp in PostgreSQL, which has no median either, only
percentile_cont(0.5). Octave has mean, median, var, and std too.
The from-scratch versions use everyday building blocks: arithmetic, a
square root, sorting, and in most languages a built-in sum. PostgreSQL also uses SQL’s basic count, min,
and max aggregates; the other languages read the minimum and maximum
straight off the sorted values. Watch for how each one sorts (needed for the
median), how it avoids integer-division traps when averaging, how much
ceremony it takes to print a number to six decimal places: Java’s
printf follows the computer’s language settings, so it passes
Locale.ROOT to keep the decimal point a point.
C, with no array that grows, doubles its own with realloc, while C++‘s
std::vector grows by itself. C++‘s std::accumulate hides a trap: start
it from 0 instead of 0.0 and it adds whole numbers, quietly dropping
every fraction. Common Lisp reads 28.6 as a single float, good to about 7
digits, unless told otherwise, and Swift won’t divide a Double by an Int until
you convert it. Prolog’s sort drops repeated values, so it sorts with
msort. In Tcl every value is a string, and a plain lsort compares
text, putting 10 before 2, so it sorts with lsort -real. Perl’s plain
sort compares text too, so it sorts with sort { $a <=> $b }. Lua’s /
always gives a float, so its mean can’t lose a fraction, and the median
finds its middle with //, which rounds down. PHP’s / gives a whole
number only when the division comes out even, so its median uses intdiv. BASIC fixes each array’s size up front with DIM and builds its
insertion sort from IF and GOTO. Pascal’s Math unit has a Variance
that divides by n − 1, but its version adds the squares up in a loop.
Haskell, with no loops, gets every total from a fold such
as sum. OCaml never mixes integers and floats, so the mean divides with
the float operator /. by float_of_int n. Elixir reads each value with Float.parse,
because String.to_float stops with an error on a number with no decimal
point, such as 12. Ada’s Long_Float'Image prints in E notation,
such as 2.96775000000000E+01, so it prints with Put and Exp => 0. Octave squares every
deviation at once with .^, where the dot means “each element”.
Data preview
| day | minutes |
|---|---|
| 1 | 28.6 |
| 2 | 43.8 |
| 3 | 16.6 |
| 4 | 42.9 |
| 5 | 20.9 |
| 6 | 22.6 |
| 7 | 35.9 |
| 8 | 28.0 |
First 8 of 120 rows · Download data.csv
Expected results
Every implementation must print these, to within a relative tolerance of 10⁻⁴.
count | 120 |
sum | 3561.300000 |
mean | 29.677500 |
median | 28.750000 |
min | 11.500000 |
max | 61.300000 |
range | 49.800000 |
variance | 83.623943 |
std_dev | 9.144613 |
Implementations
"""Descriptive statistics on one CSV column, using only the standard library.
The statistics module would do most of this in one call each; it is avoided
on purpose so the arithmetic stays visible.
"""
import csv
import math
import sys
COLUMN = "minutes"
def read_column(path, column):
with open(path, newline="") as f:
return [float(row[column]) for row in csv.DictReader(f)]
def median(sorted_values):
n = len(sorted_values)
middle = n // 2
if n % 2 == 1:
return sorted_values[middle]
return (sorted_values[middle - 1] + sorted_values[middle]) / 2
def main():
values = read_column(sys.argv[1], COLUMN)
values.sort()
n = len(values)
total = sum(values)
mean = total / n
# Sample variance divides by n - 1, not n (Bessel's correction).
variance = sum((x - mean) ** 2 for x in values) / (n - 1)
results = [
("sum", total),
("mean", mean),
("median", median(values)),
("min", values[0]),
("max", values[-1]),
("range", values[-1] - values[0]),
("variance", variance),
("std_dev", math.sqrt(variance)),
]
print(f"count {n}")
for name, value in results:
print(f"{name} {value:.6f}")
if __name__ == "__main__":
main()Python is the baseline the other languages are measured against, and this
version shows why: each formula is close to one line. csv.DictReader
reads the column by its header name, and sum with a generator expression
turns the variance formula into a single readable line.
The interesting choice is what we left out. The standard library’s
statistics module has mean, median, variance, and stdev, and
variance already divides by n − 1. We skipped it so the arithmetic stays
visible. In real code you’d reach for it.
"""Descriptive statistics on one CSV column, with the statistics module.
The built-in version of ../python/main.py: statistics.mean, median, variance,
and stdev do the arithmetic. variance and stdev are the sample versions,
dividing by n - 1.
"""
import csv
import statistics
import sys
COLUMN = "minutes"
def main():
with open(sys.argv[1], newline="") as f:
values = [float(row[COLUMN]) for row in csv.DictReader(f)]
results = [
("sum", sum(values)),
("mean", statistics.mean(values)),
("median", statistics.median(values)),
("min", min(values)),
("max", max(values)),
("range", max(values) - min(values)),
("variance", statistics.variance(values)),
("std_dev", statistics.stdev(values)),
]
print(f"count {len(values)}")
for name, value in results:
print(f"{name} {value:.6f}")
if __name__ == "__main__":
main()This is how you’d write it in real Python. The statistics module ships
with the language, and mean, median, variance, and stdev each take a
list and return one number. median sorts a copy for you and averages the
two middle values when the count is even, so 120 values need no special
case.
Watch the names. variance and stdev are the sample versions, dividing by
n − 1, which is what we want here. Their population twins, pvariance and
pstdev, divide by n, and on [1, 2, 3, 4] they give 1.25 instead of 1.67.
-- Descriptive statistics on one CSV column, using only Ada's standard
-- library.
--
-- Ada has no mean, median, or variance, so each is written out. Types are
-- strict: an Integer count must be converted with Long_Float (N) before it
-- can divide a Long_Float total.
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Long_Float_Text_IO; use Ada.Long_Float_Text_IO;
with Ada.Command_Line;
with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Numerics.Long_Elementary_Functions;
use Ada.Numerics.Long_Elementary_Functions;
with Ada.Containers.Vectors;
with Ada.Containers.Generic_Array_Sort;
procedure Main is
-- A growable list of numbers, indexed from 1.
package Float_Vectors is new Ada.Containers.Vectors (Positive, Long_Float);
type Float_Array is array (Positive range <>) of Long_Float;
procedure Sort is new Ada.Containers.Generic_Array_Sort
(Positive, Long_Float, Float_Array);
-- The text of field number Col (counting from 1) in a CSV line.
function Field (Line : String; Col : Positive) return String is
Start : Positive := Line'First;
begin
for I in 1 .. Col - 1 loop
Start := Index (Line (Start .. Line'Last), ",") + 1;
end loop;
declare
Stop : constant Natural := Index (Line (Start .. Line'Last), ",");
begin
return (if Stop = 0 then Line (Start .. Line'Last)
else Line (Start .. Stop - 1));
end;
end Field;
function Read_Column (Path, Name : String) return Float_Array is
File : File_Type;
Values : Float_Vectors.Vector;
Col : Natural := 0;
begin
Open (File, In_File, Path);
declare
Header : constant String := Get_Line (File);
begin
for C in 1 .. Ada.Strings.Fixed.Count (Header, ",") + 1 loop
if Field (Header, C) = Name then
Col := C;
end if;
end loop;
end;
if Col = 0 then
raise Constraint_Error with "column not found: " & Name;
end if;
while not End_Of_File (File) loop
Values.Append (Long_Float'Value (Field (Get_Line (File), Col)));
end loop;
Close (File);
-- Copy the vector into a fixed-size array, which Sort works on.
return Result : Float_Array (1 .. Natural (Values.Length)) do
for I in Result'Range loop
Result (I) := Values (I);
end loop;
end return;
end Read_Column;
procedure Say (Name : String; Value : Long_Float) is
begin
Put (Name & " ");
Put (Value, Fore => 1, Aft => 6, Exp => 0); -- six decimals, no E
New_Line;
end Say;
Values : Float_Array := Read_Column (Ada.Command_Line.Argument (1), "minutes");
N : constant Positive := Values'Length;
Total, Sum_Sq, Mean, Median, Variance : Long_Float := 0.0;
begin
Sort (Values);
for V of Values loop
Total := Total + V;
end loop;
Mean := Total / Long_Float (N);
-- N / 2 on two Integers rounds toward zero.
if N mod 2 = 1 then
Median := Values (N / 2 + 1);
else
Median := (Values (N / 2) + Values (N / 2 + 1)) / 2.0;
end if;
-- Sample variance divides by n - 1, not n (Bessel's correction).
for V of Values loop
Sum_Sq := Sum_Sq + (V - Mean) ** 2;
end loop;
Variance := Sum_Sq / Long_Float (N - 1);
Put_Line ("count" & Integer'Image (N));
Say ("sum", Total);
Say ("mean", Mean);
Say ("median", Median);
Say ("min", Values (1));
Say ("max", Values (N));
Say ("range", Values (N) - Values (1));
Say ("variance", Variance);
Say ("std_dev", Sqrt (Variance));
end Main;Ada’s containers and sort are generics: you make your own copy for your
types with is new, so Float_Vectors is a growable list of Long_Float,
and Sort is Generic_Array_Sort made for our array type. Types are
strict, so dividing the total by the count takes Long_Float (N) first.
Ada’s standard library has no mean, median, or variance, so each is a
loop. Sorting is a general tool. There’s no split function either, so
Field walks the commas with Index. Printing takes care too:
Long_Float'Image gives E notation, so Put with Fore, Aft, and
Exp => 0 prints six plain decimals.
⍝ Descriptive statistics on one CSV column, from scratch in GNU APL.
⍝ APL reads right to left, with no operator precedence: 2×3+4 is 2×(3+4).
⍝ ⍝ starts a comment, and ← assigns.
∇NAME SAY N
⍝ Print "name value" for a whole number. ⍕ formats a number as text.
⎕←NAME,' ',⍕N
∇
∇NAME SAY6 X;T
⍝ Print "name value" with six decimal places.
T←0 6⍕|X ⍝ |X is the size of X; 0 6⍕ gives 6 decimals
T←(∨\T≠' ')/T ⍝ drop the leading blank ⍕ adds
T←((T[1]='.')/'0'),T ⍝ ⍕ writes .94 for 0.94, so put the 0 back
⎕←NAME,' ',((X<0)/'-'),T ⍝ sign as -, since APL's own minus is ¯
∇
⍝ Read the file. ⎕ARG is the command line; ↑¯1↑ takes its last item, the
⍝ data path. ⎕FIO[26] reads the whole file as one string.
TEXT←⎕FIO[26]↑¯1↑⎕ARG
LINES←(TEXT≠⎕UCS 10)⊂TEXT ⍝ ⊂ splits at the 0s of the mask: one item per line
⍝ Split each (¨) line at commas; ⊃ stacks them into a table.
ROWS←⊃{(⍵≠',')⊂⍵}¨LINES
COL←ROWS[1;]⍳⊂'minutes' ⍝ ⍳ finds the column by its header name
⍝ Drop the header (1↓) and turn each text into a number (⍎¨).
X←⍎¨1↓ROWS[;COL]
⍝ ⍋ gives the indices that sort X; indexing with them sorts.
S←X[⍋X]
N←≢X ⍝ ≢ counts the items
MEAN←(+/X)÷N ⍝ +/ puts + between the items: the sum
MID←(N+1)÷2 ⍝ 60.5 for 120 values
MEDIAN←(+/S[(⌊MID),⌈MID])÷2 ⍝ average the values at the floor and ceiling of MID
⍝ Sample variance; right to left, so ÷N-1 divides by N-1.
VAR←(+/(X-MEAN)*2)÷N-1
'count' SAY N
'sum' SAY6 +/X
'mean' SAY6 MEAN
'median' SAY6 MEDIAN
'min' SAY6 S[1] ⍝ APL counts from 1 (⎕IO is 1)
'max' SAY6 S[N]
'range' SAY6 S[N]-S[1]
'variance' SAY6 VAR
'std_dev' SAY6 VAR*0.5 ⍝ * is power, so *0.5 is the square root
)OFFEvery statistic here is one short expression over the whole column, with no
loops. +/X is the sum, and +/(X-MEAN)*2 subtracts the mean from every
value, squares each difference, and adds them up. APL evaluates right to
left with no precedence, so ÷N-1 divides by N-1. X[⍋X] sorts: ⍋
returns the order, and indexing applies it. The median averages the values
at the floor and ceiling of (N+1)÷2, which works for odd and even counts
alike.
APL has no mean, median, or variance primitives, so there is nothing to
avoid. The real work is output: 0 6⍕ writes 0.94 as .940000 and a negative
with ¯, so we add the zero and the - ourselves.
# Descriptive statistics on one CSV column, in POSIX awk.
#
# awk reads the file one line at a time, running each pattern { action } block
# on every line it matches. POSIX awk has no sort function, so values are kept
# in order as they arrive (insertion sort).
BEGIN { FS = ","; column = "minutes" }
# The header line: find which field holds the column.
NR == 1 {
for (i = 1; i <= NF; i++)
if ($i == column) col = i
if (!col) {
print "no column " column > "/dev/stderr"
failed = 1
exit 1
}
next
}
# Every other line: insert the value into the sorted array v[1..n].
{
x = $col + 0
for (i = n; i > 0 && v[i] > x; i--)
v[i + 1] = v[i]
v[i + 1] = x
n++
total += x
}
END {
if (failed) exit 1
mean = total / n
middle = int(n / 2)
median = (n % 2 == 1) ? v[middle + 1] : (v[middle] + v[middle + 1]) / 2
# Sample variance divides by n - 1, not n (Bessel's correction).
for (i = 1; i <= n; i++)
squares += (v[i] - mean) ^ 2
variance = squares / (n - 1)
printf "count %d\n", n
printf "sum %.6f\n", total
printf "mean %.6f\n", mean
printf "median %.6f\n", median
printf "min %.6f\n", v[1]
printf "max %.6f\n", v[n]
printf "range %.6f\n", v[n] - v[1]
printf "variance %.6f\n", variance
printf "std_dev %.6f\n", sqrt(variance)
}awk was made for files like this one. It splits each line into fields for
you, so finding the minutes column in the header takes a three-line loop,
and running totals need no setup because awk starts every variable at zero.
The median is the hard part. POSIX awk has no sort function, so we insert each value into a sorted array as it’s read. That’s insertion sort, fine for 120 rows but slow for large files. In exchange, the program sticks to POSIX awk, so it runs unchanged on both awks we test: mawk, which the tests use, and the one that ships with macOS.
10 REM Descriptive statistics on one CSV column, in classic BASIC.
20 REM
30 REM There are no functions for sum, mean, median, variance, or sorting,
40 REM so each is written out. Subroutines are blocks of numbered lines
50 REM reached with GOSUB, and every variable is global.
60 REM
70 REM run.sh passes the data path in the DATA environment variable,
80 REM because bwbasic can't read command-line arguments.
100 COL$ = "minutes"
110 DIM X(1000), FL$(20) : REM arrays have a fixed size, set by DIM
120 GOSUB 1000 : REM read the column into X(1) to X(N)
130 GOSUB 2000 : REM sort it
140 T = 0
150 FOR I = 1 TO N
160 T = T + X(I)
170 NEXT I
180 M = T / N
190 REM Sample variance divides by N - 1, not N (Bessel's correction).
200 S = 0
210 FOR I = 1 TO N
220 S = S + (X(I) - M) ^ 2
230 NEXT I
240 V = S / (N - 1)
250 IF N MOD 2 = 1 THEN MED = X((N + 1) / 2) ELSE MED = (X(N / 2) + X(N / 2 + 1)) / 2
260 PRINT "count"; N
270 L$ = "sum" : Y = T : GOSUB 3000
280 L$ = "mean" : Y = M : GOSUB 3000
290 L$ = "median" : Y = MED : GOSUB 3000
300 L$ = "min" : Y = X(1) : GOSUB 3000
310 L$ = "max" : Y = X(N) : GOSUB 3000
320 L$ = "range" : Y = X(N) - X(1) : GOSUB 3000
330 L$ = "variance" : Y = V : GOSUB 3000
340 L$ = "std_dev" : Y = SQR(V) : GOSUB 3000
350 SYSTEM : REM without SYSTEM, bwbasic waits at its prompt
1000 REM Read the column named COL$ into X(1) to X(N).
1010 F$ = ENVIRON$("DATA")
1020 OPEN "I", #1, F$
1030 LINE INPUT #1, L$
1040 GOSUB 4000 : REM find which field of the header is COL$
1050 FOR K = 1 TO NF
1060 IF FL$(K) = COL$ THEN C = K
1070 NEXT K
1080 N = 0
1090 IF EOF(1) THEN 1140
1100 LINE INPUT #1, L$
1110 GOSUB 4000
1120 N = N + 1 : X(N) = VAL(FL$(C))
1130 GOTO 1090
1140 CLOSE #1
1150 RETURN
2000 REM Insertion sort: slide each value left past the larger ones.
2010 FOR I = 2 TO N
2020 Y = X(I) : J = I - 1
2030 IF J < 1 THEN 2060
2040 IF X(J) <= Y THEN 2060
2050 X(J + 1) = X(J) : J = J - 1 : GOTO 2030
2060 X(J + 1) = Y
2070 NEXT I
2080 RETURN
3000 REM Print L$ and Y with six decimals.
3010 PRINT L$; " ";
3020 PRINT USING "#.######"; Y
3030 RETURN
4000 REM Split L$ at its commas into FL$(1) to FL$(NF).
4010 NF = 0 : P = 1
4020 Q = INSTR(P, L$, ",")
4030 NF = NF + 1
4040 IF Q = 0 THEN FL$(NF) = MID$(L$, P) : RETURN
4050 FL$(NF) = MID$(L$, P, Q - P) : P = Q + 1
4060 GOTO 4020This is BASIC as many people first met it: numbered lines, GOTO, and
GOSUB. A subroutine is just a block of lines ending in RETURN. It takes
no arguments, so to print a result, the caller sets L$ and Y and then
runs GOSUB 3000. Every variable is global, and every array is given a
fixed size up front with DIM, so X holds at most 1,000 values.
There’s no sort, so lines 2000 to 2080 are an insertion sort built from
IF and GOTO. Splitting a CSV line uses INSTR and MID$, and VAL
turns the text into a number. PRINT USING "#.######" prints six decimals
and widens the field when a number needs more room.
/* Descriptive statistics on one CSV column, using only the C standard library.
*
* C has no statistics functions to avoid, and no growable arrays or CSV
* reader either: the program grows its own array with realloc and finds the
* column by splitting each line on commas. */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define COLUMN "minutes"
#define MAX_LINE 1024
/* Return the 0-based position of name in a header line, or -1. strtok
* replaces each comma with '\0' as it goes, so it works on a copy. */
static int column_index(const char *header, const char *name) {
char copy[MAX_LINE];
strcpy(copy, header);
int index = 0;
for (char *field = strtok(copy, ",\r\n"); field; field = strtok(NULL, ",\r\n")) {
if (strcmp(field, name) == 0) return index;
index++;
}
return -1;
}
/* Read one column as doubles. The array doubles in size whenever it fills,
* and the count comes back through *count. */
static double *read_column(const char *path, const char *name, size_t *count) {
FILE *file = fopen(path, "r");
if (!file) { perror(path); exit(1); }
char line[MAX_LINE];
if (!fgets(line, sizeof line, file)) { fprintf(stderr, "empty file\n"); exit(1); }
int index = column_index(line, name);
if (index < 0) { fprintf(stderr, "no column %s\n", name); exit(1); }
size_t n = 0, capacity = 64;
double *values = malloc(capacity * sizeof *values);
while (fgets(line, sizeof line, file)) {
char *field = strtok(line, ",");
for (int i = 0; i < index && field; i++) field = strtok(NULL, ",");
if (!field) continue;
if (n == capacity) {
capacity *= 2;
values = realloc(values, capacity * sizeof *values);
}
values[n++] = strtod(field, NULL);
}
fclose(file);
*count = n;
return values;
}
/* qsort calls this to compare two elements, passed as untyped pointers. */
static int compare_doubles(const void *a, const void *b) {
double x = *(const double *)a, y = *(const double *)b;
return (x > y) - (x < y);
}
int main(int argc, char **argv) {
if (argc < 2) { fprintf(stderr, "usage: %s data.csv\n", argv[0]); return 1; }
size_t n;
double *values = read_column(argv[1], COLUMN, &n);
qsort(values, n, sizeof *values, compare_doubles);
double total = 0;
for (size_t i = 0; i < n; i++) total += values[i];
double mean = total / n;
/* Arrays count from 0: for n = 120 the middle pair is values[59], values[60]. */
double median = n % 2 == 1 ? values[n / 2] : (values[n / 2 - 1] + values[n / 2]) / 2;
/* Sample variance divides by n - 1, not n (Bessel's correction). */
double squares = 0;
for (size_t i = 0; i < n; i++) squares += (values[i] - mean) * (values[i] - mean);
double variance = squares / (n - 1);
printf("count %zu\n", n);
printf("sum %.6f\n", total);
printf("mean %.6f\n", mean);
printf("median %.6f\n", median);
printf("min %.6f\n", values[0]);
printf("max %.6f\n", values[n - 1]);
printf("range %.6f\n", values[n - 1] - values[0]);
printf("variance %.6f\n", variance);
printf("std_dev %.6f\n", sqrt(variance));
free(values);
return 0;
}C is the language most of the others are written in, and you can see how
much they do for you. There’s no array that grows on its own, so
read_column starts with room for 64 values and doubles it with realloc
whenever it fills. There’s no CSV reader, so strtok cuts each line at
its commas, and strtod turns the text into a double.
Sorting is the one thing provided: qsort sorts any array, given the
element size and a comparison function that receives untyped pointers.
C has no mean or variance functions to avoid, so the statistics are
plain loops.
// Descriptive statistics on one CSV column, using only the C++ standard
// library.
//
// C++ has no mean, median, or variance. It does have std::accumulate, which
// adds up a range, and std::sort, so those do the general work here.
#include <algorithm>
#include <cmath>
#include <fstream>
#include <numeric>
#include <print>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
// Split one line at every comma.
std::vector<std::string> split(const std::string& line) {
std::vector<std::string> fields;
std::stringstream stream(line);
std::string field;
while (std::getline(stream, field, ',')) {
fields.push_back(field);
}
return fields;
}
// The named column of a CSV file, as numbers. A vector grows as we
// push_back, so there's no need to know the row count first.
std::vector<double> read_column(const std::string& path, const std::string& name) {
std::ifstream file(path);
std::string line;
std::getline(file, line);
std::vector<std::string> header = split(line);
auto found = std::find(header.begin(), header.end(), name);
if (found == header.end()) {
throw std::runtime_error("no column " + name);
}
std::size_t index = found - header.begin();
std::vector<double> values;
while (std::getline(file, line)) {
if (!line.empty()) {
values.push_back(std::stod(split(line).at(index)));
}
}
return values;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::println(stderr, "usage: program data.csv");
return 1;
}
std::vector<double> values = read_column(argv[1], "minutes");
std::sort(values.begin(), values.end());
// size() is an unsigned std::size_t; n keeps it, and every division
// below has a double on one side, so none of them rounds to a whole number.
std::size_t n = values.size();
// The 0.0 makes accumulate add doubles. A plain 0 would make it add
// ints, cutting off the fraction at every step, and no warning says so.
double sum = std::accumulate(values.begin(), values.end(), 0.0);
double mean = sum / n;
// Vectors count from 0: for n = 120 the middle pair is at 59 and 60.
double median = n % 2 == 1
? values[n / 2]
: (values[n / 2 - 1] + values[n / 2]) / 2;
// Sample variance divides by n - 1, not n (Bessel's correction).
double squares = 0.0;
for (double value : values) {
squares += (value - mean) * (value - mean);
}
double variance = squares / (n - 1);
std::println("count {}", n);
std::println("sum {:.6f}", sum);
std::println("mean {:.6f}", mean);
std::println("median {:.6f}", median);
std::println("min {:.6f}", values.front());
std::println("max {:.6f}", values.back());
std::println("range {:.6f}", values.back() - values.front());
std::println("variance {:.6f}", variance);
std::println("std_dev {:.6f}", std::sqrt(variance));
}Compared with C, the plumbing shrinks. A std::vector grows as you
push_back, so there’s no realloc, and std::getline with a comma splits
each line. std::sort orders the values for the median, and
std::accumulate adds them up. C++ has no mean, median, or variance, so
there’s nothing to skip.
Watch the 0.0 in accumulate. Its starting value sets the type it adds
in, so a plain 0 adds ints, and on 28.6, 43.8, and 11.5 it returns 82
instead of 83.9, with no warning even under -Wall -Wextra -Werror.
std::println, new in C++23, prints six decimals with {:.6f}.
*> Descriptive statistics on one CSV column, in COBOL.
*>
*> COBOL has had MEAN, MEDIAN, VARIANCE, and STANDARD-DEVIATION
*> intrinsic functions since 1989; they are avoided here so the
*> arithmetic stays visible.
IDENTIFICATION DIVISION.
PROGRAM-ID. DESCRIPTIVE-STATISTICS.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT DATA-FILE ASSIGN TO DYNAMIC WS-FILE-NAME
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD DATA-FILE.
01 DATA-LINE PIC X(200).
WORKING-STORAGE SECTION.
01 WS-FILE-NAME PIC X(256).
01 WS-COLUMN-NAME PIC X(40) VALUE "minutes".
01 WS-EOF PIC X VALUE "N".
88 END-OF-FILE VALUE "Y".
*> One CSV line split into fields, and where the column is.
01 WS-FIELDS.
05 WS-FIELD PIC X(40) OCCURS 20 TIMES.
01 WS-FIELD-COUNT PIC 99.
01 WS-POINTER PIC 999.
01 WS-LINE-LENGTH PIC 999.
01 WS-COLUMN PIC 99 VALUE 0.
*> The values: fixed-point decimals, 9 digits before the point
*> and 6 after.
01 WS-N PIC 9(4) VALUE 0.
01 WS-VALUES.
05 WS-VALUE PIC S9(9)V9(6)
OCCURS 1 TO 5000 TIMES
DEPENDING ON WS-N.
01 I PIC 9(4).
01 WS-MIDDLE PIC 9(4).
01 WS-REMAINDER PIC 9.
01 WS-SUM PIC S9(12)V9(6) VALUE 0.
01 WS-MEAN PIC S9(9)V9(12).
01 WS-MEDIAN PIC S9(9)V9(6).
01 WS-SQUARES PIC S9(12)V9(12) VALUE 0.
01 WS-VARIANCE PIC S9(9)V9(12).
01 WS-STD-DEV PIC S9(9)V9(12).
*> Output: a name, then the value edited to six decimal places.
01 WS-NAME PIC X(20).
01 WS-RESULT PIC S9(12)V9(12).
01 WS-RESULT-OUT PIC -(12)9.9(6).
01 WS-COUNT-OUT PIC Z(8)9.
PROCEDURE DIVISION.
MAIN.
ACCEPT WS-FILE-NAME FROM ARGUMENT-VALUE
OPEN INPUT DATA-FILE
READ DATA-FILE
AT END SET END-OF-FILE TO TRUE
END-READ
PERFORM SPLIT-LINE
PERFORM VARYING I FROM 1 BY 1 UNTIL I > WS-FIELD-COUNT
IF WS-FIELD(I) = WS-COLUMN-NAME
MOVE I TO WS-COLUMN
END-IF
END-PERFORM
IF WS-COLUMN = 0
DISPLAY "no column " FUNCTION TRIM(WS-COLUMN-NAME)
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF
PERFORM UNTIL END-OF-FILE
READ DATA-FILE
AT END SET END-OF-FILE TO TRUE
NOT AT END PERFORM ADD-VALUE
END-READ
END-PERFORM
CLOSE DATA-FILE
*> SORT works on tables in working storage, not just files.
SORT WS-VALUE ASCENDING
COMPUTE WS-MEAN ROUNDED = WS-SUM / WS-N
DIVIDE WS-N BY 2 GIVING WS-MIDDLE REMAINDER WS-REMAINDER
IF WS-REMAINDER = 1
MOVE WS-VALUE(WS-MIDDLE + 1) TO WS-MEDIAN
ELSE
COMPUTE WS-MEDIAN ROUNDED =
(WS-VALUE(WS-MIDDLE) + WS-VALUE(WS-MIDDLE + 1)) / 2
END-IF
*> Sample variance divides by n - 1, not n (Bessel's correction).
PERFORM VARYING I FROM 1 BY 1 UNTIL I > WS-N
COMPUTE WS-SQUARES =
WS-SQUARES + (WS-VALUE(I) - WS-MEAN) ** 2
END-PERFORM
COMPUTE WS-VARIANCE ROUNDED = WS-SQUARES / (WS-N - 1)
COMPUTE WS-STD-DEV ROUNDED = FUNCTION SQRT(WS-VARIANCE)
MOVE WS-N TO WS-COUNT-OUT
DISPLAY "count " FUNCTION TRIM(WS-COUNT-OUT)
MOVE "sum" TO WS-NAME
MOVE WS-SUM TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "mean" TO WS-NAME
MOVE WS-MEAN TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "median" TO WS-NAME
MOVE WS-MEDIAN TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "min" TO WS-NAME
MOVE WS-VALUE(1) TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "max" TO WS-NAME
MOVE WS-VALUE(WS-N) TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "range" TO WS-NAME
COMPUTE WS-RESULT = WS-VALUE(WS-N) - WS-VALUE(1)
PERFORM PRINT-RESULT
MOVE "variance" TO WS-NAME
MOVE WS-VARIANCE TO WS-RESULT
PERFORM PRINT-RESULT
MOVE "std_dev" TO WS-NAME
MOVE WS-STD-DEV TO WS-RESULT
PERFORM PRINT-RESULT
STOP RUN.
*> Split DATA-LINE on commas into WS-FIELD(1), WS-FIELD(2), ...
SPLIT-LINE.
MOVE SPACES TO WS-FIELDS
MOVE 0 TO WS-FIELD-COUNT
MOVE 1 TO WS-POINTER
MOVE FUNCTION LENGTH(FUNCTION TRIM(DATA-LINE TRAILING))
TO WS-LINE-LENGTH
PERFORM UNTIL WS-POINTER > WS-LINE-LENGTH
ADD 1 TO WS-FIELD-COUNT
UNSTRING DATA-LINE DELIMITED BY ","
INTO WS-FIELD(WS-FIELD-COUNT)
WITH POINTER WS-POINTER
END-UNSTRING
END-PERFORM.
*> Store one data line's value and add it to the running sum.
ADD-VALUE.
PERFORM SPLIT-LINE
ADD 1 TO WS-N
COMPUTE WS-VALUE(WS-N) = FUNCTION NUMVAL(WS-FIELD(WS-COLUMN))
ADD WS-VALUE(WS-N) TO WS-SUM.
*> Print "name value", rounded to six decimal places.
PRINT-RESULT.
COMPUTE WS-RESULT-OUT ROUNDED = WS-RESULT
DISPLAY FUNCTION TRIM(WS-NAME) " "
FUNCTION TRIM(WS-RESULT-OUT).COBOL does the arithmetic better than any other language here. Every number
is a fixed-point decimal with a declared size, PIC S9(9)V9(6), so the sum
of the commutes is exactly 3561.3, with no floating-point rounding at all.
Its SORT statement works on an in-memory table, so the median is easy too.
Everything around the arithmetic is long. Each variable is declared up front
with its exact width, splitting a CSV line takes an UNSTRING loop, and
printing a number means moving it into an “edited” picture and trimming the
padding. The result runs about three times as long as the Python.
One trap: COBOL has had VARIANCE and STANDARD-DEVIATION functions since
1989, but they divide by n, not n − 1. Using them would print different
numbers from every other language on this page.
;;;; Descriptive statistics on one CSV column, using only standard Common
;;;; Lisp.
;;;;
;;;; Every expression is a list with the operator first: (+ a b), (/ sum n).
;;;; Common Lisp has no mean, median, or variance, and no string split or
;;;; CSV reader, so we write those; sort, reduce, and sqrt are built in.
(defparameter *column* "minutes")
;; By default the reader turns 28.6 into a single float, which keeps only
;; about 7 digits; as a double that's 28.600000381469727. Read doubles.
;; eval-when makes the setting apply while the compiler reads this file, too,
;; not only once the program runs.
(eval-when (:compile-toplevel :load-toplevel :execute)
(setf *read-default-float-format* 'double-float))
(defun split-on-comma (line)
"The fields of LINE, split at every comma."
(loop for start = 0 then (1+ end)
for end = (position #\, line :start start)
collect (subseq line start end)
while end))
(defun parse-number (text)
"The number written in TEXT, read by the Lisp reader. Binding *read-eval*
to nil stops the reader from running code written as #.(...)."
(let ((*read-eval* nil))
(coerce (read-from-string text) 'double-float)))
(defun read-column (path name)
"The column called NAME in the CSV file at PATH, as a list of numbers."
(with-open-file (in path)
(let* ((header (split-on-comma (read-line in)))
(index (or (position name header :test #'string=)
(error "no column ~a" name))))
(loop for line = (read-line in nil)
while line
unless (string= line "")
collect (parse-number (nth index (split-on-comma line)))))))
;; sort may take the list it's given apart, so we keep only what it returns.
;; With --script, the data path is the second item of *posix-argv*.
(let* ((minutes (sort (read-column (second sb-ext:*posix-argv*) *column*) #'<))
(n (length minutes))
(sum (reduce #'+ minutes))
(mean (/ sum n))
;; Lists count from 0: for n = 120 the middle pair is at 59 and 60.
;; floor divides and rounds down, as integer division does elsewhere.
(middle (floor n 2))
(median (if (oddp n)
(nth middle minutes)
(/ (+ (nth (1- middle) minutes) (nth middle minutes)) 2)))
;; Sample variance divides by n - 1, not n (Bessel's correction).
(variance (/ (reduce #'+ (mapcar (lambda (x) (expt (- x mean) 2)) minutes))
(1- n)))
(low (first minutes))
(high (car (last minutes))))
;; ~d prints an integer, ~,6f a number with six decimals, ~% a newline.
(format t "count ~d~%" n)
(format t "sum ~,6f~%" sum)
(format t "mean ~,6f~%" mean)
(format t "median ~,6f~%" median)
(format t "min ~,6f~%" low)
(format t "max ~,6f~%" high)
(format t "range ~,6f~%" (- high low))
(format t "variance ~,6f~%" variance)
(format t "std_dev ~,6f~%" (sqrt variance)))Every expression is a list with the operator first, so the mean is
(/ sum n) and the variance sums (expt (- x mean) 2) over the list with
mapcar and reduce. Common Lisp has no mean, median, or variance to
skip, and no string split, so split-on-comma is a short loop.
Watch the first setf. The Lisp reader turns 28.6 into a single float
unless told otherwise, and without that line the sum comes out 3561.300007
and the max 61.299999. Watch sort, too: it may take apart the list you
give it, so we keep only the list it returns.
# Descriptive statistics on one CSV column, using only Elixir's standard
# library.
#
# Elixir has Enum.sum but no mean, median, or variance, so those are written
# out. Data never changes in place: each step takes a list and returns a new
# value, and |> passes a value into the next function as its first argument.
defmodule Stats do
def read_column(path, name) do
[header | rows] = path |> File.read!() |> String.split("\n", trim: true)
col = header |> String.split(",") |> Enum.find_index(&(&1 == name))
if col == nil, do: raise("column not found: #{name}")
Enum.map(rows, fn row ->
# Float.parse returns {number, rest}; "12" parses as 12.0 too.
{value, ""} = row |> String.split(",") |> Enum.at(col) |> Float.parse()
value
end)
end
def median(sorted) do
n = length(sorted)
mid = div(n, 2) # div is integer division
if rem(n, 2) == 1 do
Enum.at(sorted, mid)
else
(Enum.at(sorted, mid - 1) + Enum.at(sorted, mid)) / 2
end
end
def say(name, value) do
IO.puts("#{name} #{:erlang.float_to_binary(value, decimals: 6)}")
end
end
[path] = System.argv()
values = path |> Stats.read_column("minutes") |> Enum.sort()
n = length(values)
total = Enum.sum(values)
mean = total / n # / always gives a float
# Sample variance divides by n - 1, not n (Bessel's correction).
sumsq = values |> Enum.map(fn v -> (v - mean) ** 2 end) |> Enum.sum()
variance = sumsq / (n - 1)
min = List.first(values)
max = List.last(values)
IO.puts("count #{n}")
Stats.say("sum", total)
Stats.say("mean", mean)
Stats.say("median", Stats.median(values))
Stats.say("min", min)
Stats.say("max", max)
Stats.say("range", max - min)
Stats.say("variance", variance)
Stats.say("std_dev", :math.sqrt(variance))Elixir never changes data in place. Each step takes a list and returns a
new value, and the pipe, |>, passes a value into the next function as its
first argument, so reading the file reads top to bottom.
Elixir does have Enum.sum, a general tool, but no mean, median, or
variance, so those are written out. Enum.sort orders the values, /
always gives a float, and div is integer division, which the median uses
to find the middle. [header | rows] = ... splits off the first line by
pattern matching. The square root comes from Erlang, whose libraries
Elixir calls directly: :math.sqrt.
! Descriptive statistics on one CSV column, using only standard Fortran.
!
! Fortran has intrinsic sum, minval, and maxval for arrays, but no mean,
! median, variance, or sort, so those are written out.
program descriptive_statistics
implicit none
integer, parameter :: dp = kind(1.0d0) ! double precision
character(len=*), parameter :: column = "minutes"
real(dp), allocatable :: values(:)
real(dp) :: total, mean, variance
integer :: n
values = read_column(column)
call insertion_sort(values)
n = size(values)
total = sum(values)
mean = total / n
! Sample variance divides by n - 1, not n (Bessel's correction).
! Whole-array arithmetic: values - mean subtracts from every element.
variance = sum((values - mean)**2) / (n - 1)
print "(a, 1x, i0)", "count", n
call say("sum", total)
call say("mean", mean)
call say("median", median(values))
call say("min", values(1)) ! Fortran arrays count from 1
call say("max", values(n))
call say("range", values(n) - values(1))
call say("variance", variance)
call say("std_dev", sqrt(variance))
contains
function read_column(name) result(column_values)
character(len=*), intent(in) :: name
real(dp), allocatable :: column_values(:)
character(len=256) :: path, line, text
integer :: unit, status, col
real(dp) :: value
call get_command_argument(1, path)
open(newunit=unit, file=path, status="old", action="read")
read(unit, "(a)") line
col = field_index(line, name)
allocate(column_values(0))
do
read(unit, "(a)", iostat=status) line
if (status /= 0) exit
text = field(line, col)
read(text, *) value ! an internal read parses the number
column_values = [column_values, value] ! grows the array by one
end do
close(unit)
end function read_column
! The position of a name in a comma-separated header line.
integer function field_index(header, name)
character(len=*), intent(in) :: header, name
integer :: i, fields
fields = 1
do i = 1, len_trim(header)
if (header(i:i) == ",") fields = fields + 1
end do
do field_index = 1, fields
if (field(header, field_index) == name) return
end do
error stop "column not found: " // name
end function field_index
! The k-th comma-separated field of a line.
function field(line, k) result(text)
character(len=*), intent(in) :: line
integer, intent(in) :: k
character(len=:), allocatable :: text
integer :: start, i, comma
start = 1
do i = 1, k - 1
start = start + index(line(start:), ",")
end do
comma = index(line(start:), ",")
if (comma == 0) then
text = trim(line(start:))
else
text = line(start:start + comma - 2)
end if
end function field
subroutine insertion_sort(a)
real(dp), intent(inout) :: a(:)
real(dp) :: key
integer :: i, j
do i = 2, size(a)
key = a(i)
j = i - 1
do while (j >= 1)
if (a(j) <= key) exit
a(j + 1) = a(j)
j = j - 1
end do
a(j + 1) = key
end do
end subroutine insertion_sort
real(dp) function median(sorted)
real(dp), intent(in) :: sorted(:)
integer :: m
m = size(sorted) / 2 ! integer division
if (mod(size(sorted), 2) == 1) then
median = sorted(m + 1)
else
median = (sorted(m) + sorted(m + 1)) / 2
end if
end function median
! Print "name value" with six decimals. The f0.6 format drops the zero
! before the point (0.5 prints as .500000), so we put it back.
subroutine say(name, x)
character(len=*), intent(in) :: name
real(dp), intent(in) :: x
character(len=40) :: text
write(text, "(f0.6)") x
if (text(1:1) == ".") text = "0" // text
if (text(1:2) == "-.") text = "-0" // text(2:)
print "(a, 1x, a)", name, trim(text)
end subroutine say
end program descriptive_statisticsFortran is at home with the arithmetic: sum((values - mean)**2) works on
the whole array at once, with no loop. It has no mean, median, or
variance intrinsic, so there was nothing to skip.
What it lacks is everything around the numbers. There’s no sort, since a
call to sort fails to link, so we write an insertion sort. There’s no CSV
reader either, so field cuts a line at its commas, and an internal
read turns the text into a number. The array grows one value at a time
with column_values = [column_values, value], which reallocates it to
fit. Most of the 134 lines are this plumbing.
// Descriptive statistics on one CSV column, using only the standard library.
//
// Go has no statistics package, so the sum, mean, median, and variance are
// written out by hand.
package main
import (
"encoding/csv"
"fmt"
"math"
"os"
"slices"
"strconv"
)
const column = "minutes"
// readColumn returns one column of a CSV file, found by its header name.
func readColumn(path, name string) ([]float64, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
records, err := csv.NewReader(f).ReadAll()
if err != nil {
return nil, err
}
index := slices.Index(records[0], name)
if index < 0 {
return nil, fmt.Errorf("no column %q", name)
}
values := make([]float64, 0, len(records)-1)
for _, record := range records[1:] {
v, err := strconv.ParseFloat(record[index], 64)
if err != nil {
return nil, err
}
values = append(values, v)
}
return values, nil
}
// median expects the values already sorted.
func median(sorted []float64) float64 {
n := len(sorted)
middle := n / 2 // integer division: 120 / 2 is 60
if n%2 == 1 {
return sorted[middle]
}
return (sorted[middle-1] + sorted[middle]) / 2
}
func main() {
values, err := readColumn(os.Args[1], column)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
slices.Sort(values)
n := len(values)
total := 0.0
for _, v := range values {
total += v
}
// n is an int; Go never converts between number types implicitly.
mean := total / float64(n)
// Sample variance divides by n - 1, not n (Bessel's correction).
squares := 0.0
for _, v := range values {
squares += (v - mean) * (v - mean)
}
variance := squares / float64(n-1)
fmt.Printf("count %d\n", n)
fmt.Printf("sum %.6f\n", total)
fmt.Printf("mean %.6f\n", mean)
fmt.Printf("median %.6f\n", median(values))
fmt.Printf("min %.6f\n", values[0])
fmt.Printf("max %.6f\n", values[n-1])
fmt.Printf("range %.6f\n", values[n-1]-values[0])
fmt.Printf("variance %.6f\n", variance)
fmt.Printf("std_dev %.6f\n", math.Sqrt(variance))
}Go has no statistics package, so there are no built-ins to avoid: the sum,
mean, and variance are plain for loops, and slices.Sort orders the
values for the median. encoding/csv reads the file, and slices.Index
finds the minutes column by its header name.
The trap is types. Go never converts numbers for you, so total / n with an
int count doesn’t compile; you write total / float64(n). Integer
division truncates, which is what we want for the middle index: 120 / 2
is 60, and with an even count the median averages positions 59 and 60
(Go counts from 0). Errors come back as values, so reading the file checks
err at each step.
-- Descriptive statistics on one CSV column, using only Haskell's base library.
--
-- Haskell has no loops and no variables that change: every value below is
-- defined once, and totals come from folds such as sum. The base library has
-- no mean, median, or variance, and no CSV reader, so we split lines ourselves.
import Data.List (elemIndex, sort)
import System.Environment (getArgs)
import Text.Printf (printf)
column :: String
column = "minutes"
-- Split a line at every comma. break returns the text before the first comma
-- and the rest, which still starts with the comma.
splitOnComma :: String -> [String]
splitOnComma line = case break (== ',') line of
(field, ',' : rest) -> field : splitOnComma rest
(field, _) -> [field]
-- The named column, read as Doubles. The header gives its position.
readColumn :: String -> String -> [Double]
readColumn name text = case lines text of
header : rows
| Just index <- elemIndex name (splitOnComma header) ->
[read (splitOnComma row !! index) | row <- rows, not (null row)]
_ -> error ("no column " ++ name)
-- The middle value of a sorted list, or the mean of the two middle values.
-- Lists count from 0: for n = 120 the middle pair is positions 59 and 60.
median :: [Double] -> Double
median sorted
| odd n = sorted !! middle
| otherwise = (sorted !! (middle - 1) + sorted !! middle) / 2
where
n = length sorted
middle = n `div` 2
main :: IO ()
main = do
[path] <- getArgs
text <- readFile path
let values = sort (readColumn column text)
n = length values
-- length gives an Int; fromIntegral converts it so it can divide a Double.
count = fromIntegral n :: Double
total = sum values
mean = total / count
-- Sample variance divides by n - 1, not n (Bessel's correction). The
-- exponent's type is spelled out, or -Wall warns that it guessed Integer.
variance = sum [(x - mean) ^ (2 :: Int) | x <- values] / (count - 1)
results =
[ ("sum", total)
, ("mean", mean)
, ("median", median values)
, ("min", head values)
, ("max", last values)
, ("range", last values - head values)
, ("variance", variance)
, ("std_dev", sqrt variance)
]
printf "count %d\n" n
mapM_ (\(name, value) -> printf "%s %.6f\n" name value) resultsThere are no loops here and no variable is ever changed. Every value in
main is defined once in a let, and the totals come from sum, which
folds the list into one number. The variance is a list comprehension,
[(x - mean) ^ 2 | x <- values], which reads almost like the formula.
The plumbing is where Haskell asks for care. length returns an Int,
which can’t divide a Double, so fromIntegral converts it. There’s no
CSV reader in base, so splitOnComma splits each line with
break, calling itself on the rest. And median indexes the sorted list
with !!, which walks it from the start.
// Descriptive statistics on one CSV column, using only the Java standard
// library.
//
// Every Java program lives in a class; the runner calls Main.main. Java's
// streams have average() and summaryStatistics(), which would give the mean,
// min, and max in one call, so we write those out.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
class Main {
static final String COLUMN = "minutes";
// The named column of a CSV file, as an array of doubles. Reading a file
// can fail with an IOException, and Java makes us say so with "throws".
static double[] readColumn(Path path, String name) throws IOException {
List<String> lines = Files.readAllLines(path);
int index = Arrays.asList(lines.get(0).split(",")).indexOf(name);
if (index < 0) {
throw new IllegalArgumentException("no column " + name);
}
double[] values = new double[lines.size() - 1];
for (int row = 1; row < lines.size(); row++) {
values[row - 1] = Double.parseDouble(lines.get(row).split(",")[index]);
}
return values;
}
public static void main(String[] args) throws IOException {
double[] values = readColumn(Path.of(args[0]), COLUMN);
Arrays.sort(values);
int n = values.length;
double sum = 0;
for (double value : values) {
sum += value;
}
double mean = sum / n;
// Arrays count from 0: for n = 120 the middle pair is at 59 and 60.
// n / 2 divides two ints, so it rounds down to a whole number.
double median = n % 2 == 1
? values[n / 2]
: (values[n / 2 - 1] + values[n / 2]) / 2;
// Sample variance divides by n - 1, not n (Bessel's correction).
double squares = 0;
for (double value : values) {
squares += (value - mean) * (value - mean);
}
double variance = squares / (n - 1);
// printf follows the computer's language settings unless told
// otherwise; Locale.ROOT keeps the decimal point a point.
Locale root = Locale.ROOT;
System.out.printf(root, "count %d%n", n);
System.out.printf(root, "sum %.6f%n", sum);
System.out.printf(root, "mean %.6f%n", mean);
System.out.printf(root, "median %.6f%n", median);
System.out.printf(root, "min %.6f%n", values[0]);
System.out.printf(root, "max %.6f%n", values[n - 1]);
System.out.printf(root, "range %.6f%n", values[n - 1] - values[0]);
System.out.printf(root, "variance %.6f%n", variance);
System.out.printf(root, "std_dev %.6f%n", Math.sqrt(variance));
}
}Java wants everything spelled out. The code lives in a class named Main,
every variable has a type, and readColumn has to declare that it
throws IOException, because Java won’t compile a file read that might
fail unless you say what happens. The sums are plain for loops over a
double[], and Arrays.sort puts the values in order for the median.
Java’s streams have average() and summaryStatistics(), which would give
the mean, min, and max in one call, so we skip them. Watch printf, too:
it follows the computer’s language settings, and on a German one it prints
29,677500, so every call passes Locale.ROOT.
// Descriptive statistics on one CSV column, using only Node's built-in modules.
// JavaScript has no statistics library, so every formula is written out.
import { readFileSync } from "node:fs";
const COLUMN = "minutes";
// Node has no CSV parser. This file has no quoted fields, so splitting each
// line on commas is enough; the header row tells us which field to read.
function readColumn(path, column) {
const [header, ...lines] = readFileSync(path, "utf8").trim().split("\n");
const index = header.split(",").indexOf(column);
return lines.map((line) => Number(line.split(",")[index]));
}
function median(sortedValues) {
const n = sortedValues.length;
const middle = Math.floor(n / 2);
if (n % 2 === 1) return sortedValues[middle];
return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
}
const values = readColumn(process.argv[2], COLUMN);
// sort() compares as strings unless given a comparator: [10, 9].sort() is [10, 9].
values.sort((a, b) => a - b);
const n = values.length;
const total = values.reduce((acc, x) => acc + x, 0);
const mean = total / n;
// Sample variance divides by n - 1, not n (Bessel's correction).
const variance = values.reduce((acc, x) => acc + (x - mean) ** 2, 0) / (n - 1);
const results = [
["sum", total],
["mean", mean],
["median", median(values)],
["min", values[0]],
["max", values.at(-1)],
["range", values.at(-1) - values[0]],
["variance", variance],
["std_dev", Math.sqrt(variance)],
];
console.log(`count ${n}`);
for (const [name, value] of results) {
console.log(`${name} ${value.toFixed(6)}`);
}JavaScript makes you write two things Python hands you. Node has no CSV
parser, so we split each line on commas and find the minutes column by
its place in the header. There is no sum either, so totals come from
reduce((acc, x) => acc + x, 0).
The trap is sorting. With no comparator, sort() compares values as
strings, so [11.5, 9.5].sort() leaves 11.5 first. On this file the default
sort happens to give the right order, because every value has two digits
before the point, but we pass (a, b) => a - b so the median and minimum
don’t depend on that. JavaScript has no statistics module, so there was no
built-in to skip.
# Descriptive statistics on one CSV column, using only Base Julia.
#
# The Statistics standard library has mean, median, var, and std; they are
# avoided on purpose so the arithmetic stays visible.
using Printf
const COLUMN = "minutes"
function read_column(path, column)
lines = readlines(path)
index = findfirst(==(column), split(lines[1], ","))
return [parse(Float64, split(line, ",")[index]) for line in lines[2:end]]
end
function median_of_sorted(values)
n = length(values)
middle = n ÷ 2
# Julia counts from 1, so for even n the middle pair is middle and middle + 1.
isodd(n) ? values[middle + 1] : (values[middle] + values[middle + 1]) / 2
end
function main(path)
values = sort(read_column(path, COLUMN))
n = length(values)
total = sum(values)
mean = total / n
# Sample variance divides by n - 1, not n (Bessel's correction).
variance = sum((values .- mean) .^ 2) / (n - 1)
results = [
"sum" => total,
"mean" => mean,
"median" => median_of_sorted(values),
"min" => first(values),
"max" => last(values),
"range" => last(values) - first(values),
"variance" => variance,
"std_dev" => sqrt(variance),
]
println("count $n")
for (name, value) in results
@printf("%s %.6f\n", name, value)
end
end
main(ARGS[1])Julia’s standard library includes Statistics, with mean, median,
var, and std, and std already divides by n − 1. We skipped it so the
arithmetic stays visible. In real code you’d reach for it.
The dot does the work instead of a loop: values .- mean subtracts the mean
from every value, and .^ 2 squares each result, so the variance is one
line. The trap is indexing, as in R: Julia counts from 1, so with 120
values and middle = n ÷ 2, the middle pair is values[middle] and
values[middle + 1]. ÷ is integer division; plain / always gives a
float.
# Descriptive statistics on one CSV column, with the Statistics standard library.
#
# The built-in version of ../julia/main.jl: mean, median, var, and std do the
# arithmetic. var and std are the sample versions, dividing by n - 1, unless
# you pass corrected=false.
using Printf
using Statistics
const COLUMN = "minutes"
function read_column(path, column)
lines = readlines(path)
index = findfirst(==(column), split(lines[1], ","))
return [parse(Float64, split(line, ",")[index]) for line in lines[2:end]]
end
function main(path)
values = read_column(path, COLUMN)
low, high = extrema(values)
results = [
"sum" => sum(values),
"mean" => mean(values),
"median" => median(values),
"min" => low,
"max" => high,
"range" => high - low,
"variance" => var(values),
"std_dev" => std(values),
]
println("count $(length(values))")
for (name, value) in results
@printf("%s %.6f\n", name, value)
end
end
main(ARGS[1])Statistics is one of Julia’s standard libraries: it ships with Julia but
has to be loaded with using, like Printf. It supplies mean, median,
var, and std. sum and extrema are in Base Julia; extrema returns
the minimum and maximum together.
By default var and std are the sample versions, dividing by n − 1.
Passing corrected=false switches to dividing by n, so on [1, 2, 3, 4]
var gives 1.67 and var(x, corrected=false) gives 1.25. Unlike the
from-scratch version, nothing here sorts the data first: median does
that internally.
-- Descriptive statistics on one CSV column, using only Lua's standard
-- library.
--
-- Lua has no mean, median, or variance, so each is written out. Its one data
-- structure is the table, used here as an array: indexes start at 1, and #t
-- is the length.
local function read_column(path, name)
local f = assert(io.open(path, "r"))
local header, col = f:read("l"), nil
local i = 0
for field in header:gmatch("[^,]+") do
i = i + 1
if field == name then col = i end
end
assert(col, "column not found: " .. name)
local values = {}
for line in f:lines() do
local j = 0
for field in line:gmatch("[^,]+") do
j = j + 1
if j == col then values[#values + 1] = tonumber(field) end
end
end
f:close()
return values
end
local function median(sorted)
local n = #sorted
local mid = n // 2 -- // is integer (floor) division
if n % 2 == 1 then
return sorted[mid + 1]
end
return (sorted[mid] + sorted[mid + 1]) / 2
end
local function say(name, value)
print(string.format("%s %.6f", name, value))
end
local values = read_column(arg[1], "minutes")
table.sort(values) -- sorts in place, smallest first
local n = #values
local total = 0
for _, v in ipairs(values) do
total = total + v
end
local mean = total / n -- / always gives a float
-- Sample variance divides by n - 1, not n (Bessel's correction).
local sumsq = 0
for _, v in ipairs(values) do
sumsq = sumsq + (v - mean) ^ 2
end
local variance = sumsq / (n - 1)
local min, max = values[1], values[n]
print("count " .. n)
say("sum", total)
say("mean", mean)
say("median", median(values))
say("min", min)
say("max", max)
say("range", max - min)
say("variance", variance)
say("std_dev", math.sqrt(variance))Lua keeps everything in tables. Here a table is an array of the commute
times: indexes start at 1, #values is its length, and values[n] is the
last one. ipairs walks it in order.
Lua’s standard library has no mean, median, or variance, so each is a
loop. Sorting is a general tool: table.sort orders the values in place.
There’s no CSV reader or split function either, so gmatch("[^,]+"), a
Lua pattern, finds each field between commas, and tonumber turns it into
a number. Watch the two divisions: / always gives a float, while //
rounds down to an integer, which the median uses to find the middle.
(* Descriptive statistics on one CSV column, using only OCaml's standard
library.
OCaml has no mean, median, or variance, so each is written out. Integers
and floats never mix: floats have their own operators, +. -. *. /., and
float_of_int converts a count before dividing. *)
let read_column path name =
In_channel.with_open_text path (fun ic ->
let lines = In_channel.input_lines ic in
let header = String.split_on_char ',' (List.hd lines) in
(* find_index returns an option: Some i, or None if it's missing *)
let col =
match List.find_index (String.equal name) header with
| Some i -> i
| None -> failwith ("column not found: " ^ name)
in
List.map
(fun line -> float_of_string (List.nth (String.split_on_char ',' line) col))
(List.tl lines))
let median sorted =
let n = Array.length sorted in
let mid = n / 2 in (* / on two ints rounds toward zero *)
if n mod 2 = 1 then sorted.(mid)
else (sorted.(mid - 1) +. sorted.(mid)) /. 2.
let say name value = Printf.printf "%s %.6f\n" name value
let () =
let values = Array.of_list (read_column Sys.argv.(1) "minutes") in
Array.sort compare values; (* sorts the array in place *)
let n = Array.length values in
let total = Array.fold_left ( +. ) 0. values in
let mean = total /. float_of_int n in
(* Sample variance divides by n - 1, not n (Bessel's correction). *)
let sumsq = Array.fold_left (fun acc v -> acc +. ((v -. mean) ** 2.)) 0. values in
let variance = sumsq /. float_of_int (n - 1) in
let min = values.(0) and max = values.(n - 1) in
Printf.printf "count %d\n" n;
say "sum" total;
say "mean" mean;
say "median" (median values);
say "min" min;
say "max" max;
say "range" (max -. min);
say "variance" variance;
say "std_dev" (sqrt variance)OCaml checks types before the program runs, and it never mixes integers
and floats. Floats get their own operators, +., -., and /., so
dividing the total by the count takes float_of_int n first. Writing
total / n won’t compile.
OCaml’s standard library has no mean, median, or variance, so each is
written out. Array.fold_left adds the values up, and sorting is a
general tool: Array.sort compare orders them in place. Reading the file
takes In_channel.input_lines, String.split_on_char cuts each line at
its commas, and List.find_index returns Some position, or None when
the column is missing, which the match must handle.
% Descriptive statistics on one CSV column, using only core GNU Octave.
%
% Octave works on whole arrays: sum(values) adds every element, and
% values - mean_value subtracts from each one. It also has mean, median, var, and
% std, but those are the method itself, so we write each one out.
% The named column of a CSV file, as a column vector of numbers.
function values = read_column(path, name)
lines = strsplit(strtrim(fileread(path)), "\n");
header = strsplit(strtrim(lines{1}), ",");
index = find(strcmp(header, name));
if isempty(index)
error("no column %s", name);
end
values = zeros(numel(lines) - 1, 1);
for row = 2:numel(lines)
fields = strsplit(strtrim(lines{row}), ",");
values(row - 1) = str2double(fields{index});
end
end
args = argv();
values = sort(read_column(args{1}, "minutes"));
n = numel(values);
total = sum(values);
mean_value = total / n;
% Arrays count from 1: for n = 120 the middle pair is positions 60 and 61.
if mod(n, 2) == 1
median_value = values((n + 1) / 2);
else
median_value = (values(n / 2) + values(n / 2 + 1)) / 2;
end
% Sample variance divides by n - 1, not n (Bessel's correction). The .^
% squares each element; plain ^ would mean a matrix power.
variance = sum((values - mean_value) .^ 2) / (n - 1);
printf("count %d\n", n);
printf("sum %.6f\n", total);
printf("mean %.6f\n", mean_value);
printf("median %.6f\n", median_value);
printf("min %.6f\n", values(1));
printf("max %.6f\n", values(end));
printf("range %.6f\n", values(end) - values(1));
printf("variance %.6f\n", variance);
printf("std_dev %.6f\n", sqrt(variance));Octave works on whole arrays, so most lines here are one formula. sum
adds every value, and (values - mean_value) .^ 2 squares every
difference at once. The dot matters: .^ squares each element, while ^
alone would ask for a matrix power.
Octave ships mean, median, var, and std, which would do this whole
page in four calls, so we write each one out instead. There’s no CSV
reader that looks up a column by name either: read_column splits the
file with strsplit and finds the header with strcmp. Arrays count from
1, so the middle pair of 120 values is at positions 60 and 61.
{ Descriptive statistics on one CSV column, using only Free Pascal's
run-time library.
The Math unit has Mean, Variance, and StdDev, but they are left out here,
so every statistic is written out. Sorting uses TArrayHelper from
Generics.Collections. }
program DescriptiveStatistics;
{$mode objfpc}{$H+} // 32-bit Integer, and strings longer than 255 characters
uses
SysUtils, Generics.Collections;
type
TDoubleArray = array of Double;
function ReadColumn(const Path, Name: String): TDoubleArray;
var
F: TextFile;
Line: String;
Fields: TStringArray;
Col, I: Integer;
begin
Result := nil;
AssignFile(F, Path);
Reset(F);
ReadLn(F, Line);
Fields := Line.Split(',');
Col := -1;
for I := 0 to High(Fields) do
if Fields[I] = Name then
Col := I;
if Col < 0 then
raise Exception.Create('column not found: ' + Name);
while not Eof(F) do
begin
ReadLn(F, Line);
Fields := Line.Split(',');
SetLength(Result, Length(Result) + 1); // grow by one
Result[High(Result)] := StrToFloat(Fields[Col]);
end;
CloseFile(F);
end;
function Median(const Sorted: TDoubleArray): Double;
var
N: Integer;
begin
N := Length(Sorted);
if Odd(N) then
Result := Sorted[N div 2]
else
Result := (Sorted[N div 2 - 1] + Sorted[N div 2]) / 2;
end;
procedure Say(const Name: String; Value: Double);
begin
WriteLn(Name, ' ', Value:0:6); // width 0, six decimals
end;
var
Values: TDoubleArray;
N, I: Integer;
Total, Mean, SumSq, Variance: Double;
begin
Values := ReadColumn(ParamStr(1), 'minutes');
specialize TArrayHelper<Double>.Sort(Values);
N := Length(Values);
Total := 0;
for I := 0 to N - 1 do
Total := Total + Values[I];
Mean := Total / N; // / always gives a real; div is for integers
// Sample variance divides by n - 1, not n (Bessel's correction).
SumSq := 0;
for I := 0 to N - 1 do
SumSq := SumSq + Sqr(Values[I] - Mean);
Variance := SumSq / (N - 1);
WriteLn('count ', N);
Say('sum', Total);
Say('mean', Mean);
Say('median', Median(Values));
Say('min', Values[0]);
Say('max', Values[N - 1]);
Say('range', Values[N - 1] - Values[0]);
Say('variance', Variance);
Say('std_dev', Sqrt(Variance));
end.Free Pascal’s Math unit has Mean, Variance, and StdDev, and its
Variance divides by n − 1 like ours, but we leave them out and write
each statistic as a loop. Sorting is a general tool, so
TArrayHelper<Double>.Sort from Generics.Collections orders the values
for the median.
Pascal declares every variable before the begin of its block, with its
type. ReadColumn grows a dynamic array one value at a time with
SetLength, splits each line with the string helper Split, and parses
it with StrToFloat. / always gives a real number and div is for
whole numbers, so the mean needs no conversion. Value:0:6 prints six
decimals.
# Descriptive statistics on one CSV column, using only Perl and its core
# modules.
#
# Perl has no mean, median, or variance, so each is written out. List::Util,
# a core module, supplies sum. A name's first character, its sigil, says
# what it holds: $ a single value, @ an array, % a hash.
use strict;
use warnings;
use List::Util qw(sum);
sub read_column {
my ($path, $name) = @_;
open my $fh, '<', $path or die "can't open $path: $!";
chomp(my $header = <$fh>);
my @fields = split /,/, $header;
my ($col) = grep { $fields[$_] eq $name } 0 .. $#fields;
die "column not found: $name" unless defined $col;
my @values;
while (my $line = <$fh>) {
chomp $line;
push @values, (split /,/, $line)[$col];
}
close $fh;
return @values;
}
sub median {
my @sorted = @_;
my $n = @sorted; # an array used as a number is its length
my $mid = int($n / 2);
return $sorted[$mid] if $n % 2 == 1;
return ($sorted[$mid - 1] + $sorted[$mid]) / 2;
}
sub say_value {
my ($name, $value) = @_;
printf "%s %.6f\n", $name, $value;
}
# A plain sort compares text, putting 10 before 9, so <=> compares numbers.
my @values = sort { $a <=> $b } read_column($ARGV[0], 'minutes');
my $n = @values;
my $total = sum(@values);
my $mean = $total / $n;
# Sample variance divides by n - 1, not n (Bessel's correction).
my $sumsq = sum(map { ($_ - $mean) ** 2 } @values);
my $variance = $sumsq / ($n - 1);
my ($min, $max) = ($values[0], $values[-1]);
print "count $n\n";
say_value('sum', $total);
say_value('mean', $mean);
say_value('median', median(@values));
say_value('min', $min);
say_value('max', $max);
say_value('range', $max - $min);
say_value('variance', $variance);
say_value('std_dev', sqrt $variance);Perl marks every name with a sigil: @values is the array of commute
times, $values[0] is one of them, and $n = @values gives the count,
because an array used as a single value is its length.
Perl has no mean, median, or variance, so each is written out. sum comes
from List::Util, a core module that ships with Perl. Sorting is a general
tool, but watch the block: a plain sort compares text, putting 10 before
9, so sort { $a <=> $b } compares numbers. There’s no CSV reader, so
split /,/ breaks each line on commas.
<?php
// Descriptive statistics on one CSV column, using only PHP and the
// extensions built into it.
//
// PHP has no mean, median, or variance, so each is written out. A PHP array
// is an ordered map; here its keys are 0, 1, 2, and so on, so it acts as a
// list.
declare(strict_types=1);
function read_column(string $path, string $name): array
{
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$header = explode(',', array_shift($lines));
$col = array_search($name, $header, true);
if ($col === false) {
throw new RuntimeException("column not found: $name");
}
$values = [];
foreach ($lines as $line) {
$values[] = (float) explode(',', $line)[$col];
}
return $values;
}
function median(array $sorted): float
{
$n = count($sorted);
$mid = intdiv($n, 2);
if ($n % 2 === 1) {
return $sorted[$mid];
}
return ($sorted[$mid - 1] + $sorted[$mid]) / 2;
}
function say(string $name, float $value): void
{
printf("%s %.6f\n", $name, $value);
}
$values = read_column($argv[1], 'minutes');
sort($values); // sorts in place, smallest first
$n = count($values);
$total = array_sum($values);
$mean = $total / $n;
// Sample variance divides by n - 1, not n (Bessel's correction).
$sumsq = array_sum(array_map(fn($v) => ($v - $mean) ** 2, $values));
$variance = $sumsq / ($n - 1);
$min = $values[0];
$max = $values[$n - 1];
echo "count $n\n";
say('sum', $total);
say('mean', $mean);
say('median', median($values));
say('min', $min);
say('max', $max);
say('range', $max - $min);
say('variance', $variance);
say('std_dev', sqrt($variance));A PHP array is an ordered map from keys to values. With the keys 0, 1, 2,
and so on, as here, it acts as a list: $values[0] is the smallest time
once sort has put them in order, and count gives their number.
PHP has no mean, median, or variance, so each is written out, with
array_sum for the totals. array_map and a short fn square each
deviation. PHP does have a CSV reader, str_getcsv, but no field
here is quoted, so explode simply splits each line on commas. intdiv finds the median’s middle, because / gives
a whole number only when the division comes out even: 10 / 2 is 5, but
9 / 2 is 4.5.
-- Descriptive statistics on one column, in PostgreSQL.
--
-- run.sh loads data.csv into a table named data. PostgreSQL has avg(),
-- var_samp(), stddev_samp(), and percentile_cont() built in; they are avoided
-- here so the arithmetic stays visible.
WITH totals AS (
SELECT count(*) AS n, sum(minutes) AS total, min(minutes) AS lo, max(minutes) AS hi
FROM data
),
mean AS (
SELECT total / n AS mean FROM totals
),
variance AS (
-- Sample variance divides by n - 1, not n (Bessel's correction).
SELECT sum((minutes - mean) ^ 2) / (n - 1) AS variance
FROM data, mean, totals
GROUP BY n
),
ranked AS (
SELECT minutes, row_number() OVER (ORDER BY minutes) AS position
FROM data
),
median AS (
-- The middle value, or the mean of the two middle values when n is even.
-- Integer division: for n = 120 this picks positions 60 and 61.
SELECT sum(minutes) / count(*) AS median
FROM ranked, totals
WHERE position IN ((n + 1) / 2, (n + 2) / 2)
)
SELECT name, value
FROM totals, mean, variance, median,
LATERAL (VALUES
(1, 'count', n::text),
(2, 'sum', round(total::numeric, 6)::text),
(3, 'mean', round(mean::numeric, 6)::text),
(4, 'median', round(median::numeric, 6)::text),
(5, 'min', round(lo::numeric, 6)::text),
(6, 'max', round(hi::numeric, 6)::text),
(7, 'range', round((hi - lo)::numeric, 6)::text),
(8, 'variance', round(variance::numeric, 6)::text),
(9, 'std_dev', round(sqrt(variance)::numeric, 6)::text)
) AS results (position, name, value)
ORDER BY position;In SQL you describe results instead of writing loops. Count, sum, minimum,
and maximum are basic aggregates, and named common table expressions
(WITH totals AS (...)) let the query read from top to bottom like a
script.
The median is where SQL resists. A table has no “middle row” until you
number the rows with a window function, row_number() OVER (ORDER BY minutes), and pick positions 60 and 61. PostgreSQL could do it in one call
with percentile_cont(0.5) WITHIN GROUP (ORDER BY minutes), and avg,
var_samp, and stddev_samp cover the rest. We avoided them here.
-- Descriptive statistics on one column, with PostgreSQL's aggregates.
--
-- The built-in version of ../postgresql/main.sql. run.sh loads data.csv into
-- a table named data. avg(), var_samp(), and stddev_samp() do the arithmetic,
-- and percentile_cont(0.5) is the median, averaging the two middle values
-- when there is an even number of rows.
WITH stats AS (
SELECT
count(*) AS n,
sum(minutes) AS total,
avg(minutes) AS mean,
percentile_cont(0.5) WITHIN GROUP (ORDER BY minutes) AS median,
min(minutes) AS lo,
max(minutes) AS hi,
var_samp(minutes) AS variance,
stddev_samp(minutes) AS std_dev
FROM data
)
SELECT name, value
FROM stats,
LATERAL (VALUES
(1, 'count', n::text),
(2, 'sum', round(total::numeric, 6)::text),
(3, 'mean', round(mean::numeric, 6)::text),
(4, 'median', round(median::numeric, 6)::text),
(5, 'min', round(lo::numeric, 6)::text),
(6, 'max', round(hi::numeric, 6)::text),
(7, 'range', round((hi - lo)::numeric, 6)::text),
(8, 'variance', round(variance::numeric, 6)::text),
(9, 'std_dev', round(std_dev::numeric, 6)::text)
) AS results (position, name, value)
ORDER BY position;Every statistic here is one of PostgreSQL’s aggregate functions, so a single
SELECT over the table computes them all in one pass. avg, var_samp,
and stddev_samp are the sample versions (var_pop and stddev_pop
divide by n instead).
The median is the odd one out. There’s no median function; instead you
ask for the 50th percentile with percentile_cont(0.5) WITHIN GROUP (ORDER BY minutes). The cont means continuous: with an even number of rows it
interpolates between the two middle values, which is the usual median. Its
sibling percentile_disc would return one of them instead.
% Descriptive statistics on one CSV column, using SWI-Prolog and the
% libraries that come with it.
%
% A Prolog program is facts and rules, not steps. A rule such as
% median(Sorted, Median) is true when Median is the median of Sorted, and
% running the program asks Prolog to find values that make main true.
% There's no mean, median, or variance built in, so we state each one.
:- initialization(main, main).
column("minutes").
% read_column(+Path, +Name, -Values): Values are the numbers in the column
% called Name. findall collects every Value that satisfies the goals in
% the middle, one per line.
read_column(Path, Name, Values) :-
read_file_to_string(Path, Text, []),
split_string(Text, "\n", "", [HeaderLine|Lines]),
split_string(HeaderLine, ",", "", Header),
nth0(Index, Header, Name),
findall(Value,
( member(Line, Lines),
Line \== "",
split_string(Line, ",", "", Fields),
nth0(Index, Fields, Field),
number_string(Value, Field) ),
Values).
% median(+Sorted, -Median): the middle value, or the mean of the two
% middle values. nth0 counts from 0: for 120 values, positions 59 and 60.
median(Sorted, Median) :-
length(Sorted, N),
Middle is N // 2,
( N mod 2 =:= 1
-> nth0(Middle, Sorted, Median)
; Before is Middle - 1,
nth0(Before, Sorted, A),
nth0(Middle, Sorted, B),
Median is (A + B) / 2
).
% squared_deviation(+Mean, +X, -D): D is (X - Mean) squared. "is" does the
% arithmetic; D = X - Mean would only build the term X - Mean.
squared_deviation(Mean, X, D) :-
D is (X - Mean) ** 2.
main :-
current_prolog_flag(argv, [Path|_]),
column(Name),
read_column(Path, Name, Unsorted),
% msort keeps repeated values; sort would drop them.
msort(Unsorted, Values),
length(Values, N),
sum_list(Values, Sum),
Mean is Sum / N,
median(Values, Median),
Values = [Min|_],
last(Values, Max),
Range is Max - Min,
% Sample variance divides by n - 1, not n (Bessel's correction).
maplist(squared_deviation(Mean), Values, Squares),
sum_list(Squares, SumOfSquares),
Variance is SumOfSquares / (N - 1),
StdDev is sqrt(Variance),
format("count ~d~n", [N]),
forall(member(Label-Number,
[sum-Sum, mean-Mean, median-Median, min-Min, max-Max,
range-Range, variance-Variance, std_dev-StdDev]),
format("~w ~6f~n", [Label, Number])).Prolog states what’s true rather than listing steps. median(Sorted, Median) holds when Median is the middle of Sorted, and running the
program asks Prolog to find values that make main true. There’s no
mean, median, or variance to skip, so each is a short rule, and maplist
applies squared_deviation to every value.
Two traps hide here. D = X - Mean doesn’t subtract: it makes D the
term X - Mean, and only is does arithmetic. And sort drops repeated
values. Our 120 minutes hold just 100 distinct ones, so sort would get
the count, mean, and median wrong; msort keeps every value.
# Descriptive statistics on one CSV column, using only base R.
#
# R has mean(), median(), var(), and sd() built in; they are avoided here so
# the arithmetic stays visible.
column <- "minutes"
args <- commandArgs(trailingOnly = TRUE)
values <- sort(read.csv(args[1])[[column]])
n <- length(values)
total <- sum(values)
mean_value <- total / n
# Vectors are 1-indexed: for n = 120 the middle pair is values[60], values[61].
middle <- n %/% 2
median_value <- if (n %% 2 == 1) values[middle + 1] else (values[middle] + values[middle + 1]) / 2
# Sample variance divides by n - 1, not n (Bessel's correction).
# values - mean_value subtracts the mean from every element at once.
variance <- sum((values - mean_value)^2) / (n - 1)
results <- c(
sum = total,
mean = mean_value,
median = median_value,
min = values[1],
max = values[n],
range = values[n] - values[1],
variance = variance,
std_dev = sqrt(variance)
)
cat(sprintf("count %d\n", n))
cat(sprintf("%s %.6f\n", names(results), results), sep = "")R was built for exactly this, so the from-scratch version is the long way
around. mean(), median(), var(), and sd() are all built in, and
var() already divides by n − 1. We avoided them to show the arithmetic.
Even so, vectorization keeps it short. values - mean_value subtracts the
mean from every value at once, so the variance needs no loop, and one
sprintf call formats all eight results together. The trap is indexing:
R counts from 1, so the two middle values of 120 are values[60] and
values[61].
# Descriptive statistics on one CSV column, with R's built-in functions.
#
# The built-in version of ../r/main.R: mean(), median(), var(), and sd() do
# the arithmetic. var() and sd() are the sample versions, dividing by n - 1.
column <- "minutes"
args <- commandArgs(trailingOnly = TRUE)
values <- read.csv(args[1])[[column]]
results <- c(
sum = sum(values),
mean = mean(values),
median = median(values),
min = min(values),
max = max(values),
range = diff(range(values)),
variance = var(values),
std_dev = sd(values)
)
cat(sprintf("count %d\n", length(values)))
cat(sprintf("%s %.6f\n", names(results), results), sep = "")Every statistic here is a base R function. R was built for this, so there’s
nothing to import: mean, median, var, and sd are there the moment R
starts, and each takes the whole column at once.
Two details are worth knowing. var and sd are the sample versions,
dividing by n − 1, and base R has no population version at all. And
range doesn’t return a number: it returns the minimum and maximum as a
pair, so we take diff of that pair to get the width the from-scratch
version computes as max - min.
# Descriptive statistics on one CSV column, using only the standard library.
COLUMN = "minutes"
# The csv library is a bundled gem since Ruby 3.4, not standard library, so
# split lines by hand. Fine for this data: no quoted fields or embedded commas.
def read_column(path, column)
header, *rows = File.readlines(path, chomp: true)
index = header.split(",").index(column) or abort "no column #{column}"
rows.map { |row| Float(row.split(",")[index]) }
end
def median(sorted)
middle = sorted.length / 2
return sorted[middle] if sorted.length.odd?
(sorted[middle - 1] + sorted[middle]) / 2
end
values = read_column(ARGV[0], COLUMN).sort
n = values.length
total = values.sum
mean = total / n
# Sample variance divides by n - 1, not n (Bessel's correction).
variance = values.sum { |x| (x - mean)**2 } / (n - 1)
results = {
sum: total,
mean: mean,
median: median(values),
min: values.first,
max: values.last,
range: values.last - values.first,
variance: variance,
std_dev: Math.sqrt(variance),
}
puts "count #{n}"
results.each { |name, value| puts format("%s %.6f", name, value) }Ruby reads almost like the math. values.sum { |x| (x - mean)**2 } is the
variance’s numerator in one expression, and a hash keeps the results in
insertion order, so printing them in the right order is a single loop.
The awkward part is reading the file. Since Ruby 3.4, csv has been a
bundled gem rather than part of the standard library, so we split each line
on commas by hand. That works for this file but would break on quoted
fields. Watch integer division too: 7 / 2 is 3 in Ruby. It’s safe here
only because the values are already floats.
//! Descriptive statistics on one CSV column, using only the standard library.
//!
//! Rust's standard library has no statistics functions at all, so every
//! formula is written out. There is no CSV parser either; the file has no
//! quoted fields, so splitting each line on commas is enough.
use std::env;
use std::fs;
const COLUMN: &str = "minutes";
/// Reads one column, found by its header name, as floating-point numbers.
fn read_column(path: &str, column: &str) -> Vec<f64> {
let text = fs::read_to_string(path).expect("cannot read data file");
let mut lines = text.lines();
let header: Vec<&str> = lines.next().expect("empty file").split(',').collect();
let index = header
.iter()
.position(|&name| name == column)
.expect("column not found");
lines
.map(|line| line.split(',').nth(index).unwrap().parse().unwrap())
.collect()
}
fn median(sorted: &[f64]) -> f64 {
let n = sorted.len();
let middle = n / 2;
if n % 2 == 1 {
sorted[middle]
} else {
(sorted[middle - 1] + sorted[middle]) / 2.0
}
}
fn main() {
let path = env::args().nth(1).expect("usage: main <data.csv>");
let mut values = read_column(&path, COLUMN);
// f64 has no total order (NaN), so plain sort() won't compile;
// total_cmp supplies one.
values.sort_by(f64::total_cmp);
let n = values.len();
let total: f64 = values.iter().sum();
let mean = total / n as f64;
// Sample variance divides by n - 1, not n (Bessel's correction).
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
let (min, max) = (values[0], values[n - 1]);
let results = [
("sum", total),
("mean", mean),
("median", median(&values)),
("min", min),
("max", max),
("range", max - min),
("variance", variance),
("std_dev", variance.sqrt()),
];
println!("count {n}");
for (name, value) in results {
println!("{name} {value:.6}");
}
}Rust makes you say what you mean about numbers. values.sort() won’t
compile on f64, because NaN compares unequal to everything and so floats
have no total order; sort_by(f64::total_cmp) supplies one. Dividing a
float by the usize count is also a compile error, so each count is
converted with as f64 first. And sum() sometimes needs its type spelled
out, as sum::<f64>(), before the compiler will accept it.
There was no built-in to skip: the standard library has no mean, median, or
variance at all. It has no CSV parser either, so we split each line on
commas and find the minutes column by its header name.
// Descriptive statistics on one CSV column, using only Swift's standard
// library and Foundation, which ships with it.
//
// Swift has no mean, median, or variance, so we write them. Foundation reads
// the file and formats the numbers; the arithmetic is plain Swift.
import Foundation
let column = "minutes"
// The named column of a CSV file, as numbers. A function that can fail is
// marked throws, and every call to it is marked try.
func readColumn(_ path: String, _ name: String) throws -> [Double] {
let text = try String(contentsOfFile: path, encoding: .utf8)
// split skips empty pieces, so a blank last line doesn't become a row.
var lines = text.split(separator: "\n")
let header = lines.removeFirst().split(separator: ",").map(String.init)
guard let index = header.firstIndex(of: name) else {
fatalError("no column \(name)")
}
// Double(_:) gives nil for text that isn't a number, and ! stops the
// program there instead of carrying on with a missing value.
return lines.map { line in
Double(line.split(separator: ",", omittingEmptySubsequences: false)[index])!
}
}
let values = try readColumn(CommandLine.arguments[1], column).sorted()
let n = values.count
// Swift never mixes Int and Double on its own; Double(n) converts the count.
let count = Double(n)
let sum = values.reduce(0, +)
let mean = sum / count
// Arrays count from 0: for n = 120 the middle pair is at 59 and 60.
let median = n % 2 == 1
? values[n / 2]
: (values[n / 2 - 1] + values[n / 2]) / 2
// Sample variance divides by n - 1, not n (Bessel's correction).
let variance = values.map { ($0 - mean) * ($0 - mean) }.reduce(0, +) / (count - 1)
func show(_ name: String, _ value: Double) {
print(name, String(format: "%.6f", value))
}
print("count", n)
show("sum", sum)
show("mean", mean)
show("median", median)
show("min", values.first!)
show("max", values.last!)
show("range", values.last! - values.first!)
show("variance", variance)
show("std_dev", variance.squareRoot())Swift has no mean, median, or variance, so we write them with
reduce(0, +) for the sums and sorted() for the median. Foundation,
which ships with Swift, reads the file and formats six decimals with
String(format:).
Swift is strict about types. It never mixes Int and Double on its own,
so sum / n won’t compile until Double(n) converts the count. Missing
values are spelled out, too: Double("28.6") returns an optional that
might be nil, and the ! says we’re sure it isn’t, stopping the
program if we’re wrong. A function that can fail says throws, and each
call says try.
# Descriptive statistics on one CSV column, using only Tcl's built-in
# commands.
#
# Tcl has no mean, median, or variance (those live in Tcllib, which isn't
# installed), so each is written out. lsort sorts the values, and expr does
# the arithmetic; its {braces} keep Tcl from substituting twice.
proc read_column {path name} {
set f [open $path r]
set header [split [gets $f] ,]
set col [lsearch -exact $header $name]
if {$col < 0} { error "column not found: $name" }
set values {}
while {[gets $f line] >= 0} {
lappend values [lindex [split $line ,] $col]
}
close $f
return $values
}
proc median {sorted} {
set n [llength $sorted]
set mid [expr {$n / 2}] ;# integer division, since both are integers
if {$n % 2 == 1} {
return [lindex $sorted $mid]
}
return [expr {([lindex $sorted $mid-1] + [lindex $sorted $mid]) / 2.0}]
}
proc say {name value} {
puts "$name [format %.6f $value]"
}
# Every value is a string, so plain lsort would put 10 before 2; -real
# compares them as numbers.
set values [lsort -real [read_column [lindex $argv 0] minutes]]
set n [llength $values]
set total 0.0
foreach v $values {
set total [expr {$total + $v}]
}
set mean [expr {$total / $n}]
# Sample variance divides by n - 1, not n (Bessel's correction).
set sumsq 0.0
foreach v $values {
set sumsq [expr {$sumsq + ($v - $mean) ** 2}]
}
set variance [expr {$sumsq / ($n - 1)}]
set min [lindex $values 0]
set max [lindex $values end]
puts "count $n"
say sum $total
say mean $mean
say median [median $values]
say min $min
say max $max
say range [expr {$max - $min}]
say variance $variance
say std_dev [expr {sqrt($variance)}]In Tcl, every value is a string, and commands do all the work. set
assigns, foreach loops, and arithmetic happens only inside expr. Square
brackets run a command and put its result in place, so
[lindex $values end] reads the last value.
Tcl’s own commands have no mean, median, or variance. Tcllib has them in
math::statistics, but it isn’t installed, so each is a loop. Sorting is a
general tool: lsort -real orders the values as numbers, since a plain
lsort compares text and would put 10 before 2. split cuts each line at
its commas, and format %.6f prints six decimals.