Вопрос или проблема
У меня есть список чисел в файле, одно на строку. Как я могу получить минимальное, максимальное, медианное и среднее значения? Я хочу использовать результаты в bash скрипте.
Хотя моя текущая задача состоит в работе с целыми числами, решение для чисел с плавающей запятой может быть полезным в дальнейшем, но простой метод для целых чисел тоже подойдет.
С использованием GNU datamash:
$ printf '%s\n' 1 2 4 | datamash max 1 min 1 mean 1 median 1
4 1 2.3333333333333 2
Вы можете использовать язык программирования R.
Вот простой R скрипт:
#! /usr/bin/env Rscript
d<-scan("stdin", quiet=TRUE)
cat(min(d), max(d), median(d), mean(d), sep="\n")
Заметьте, использование "stdin"
в функции scan
позволяет читать из стандартного ввода (что значит из пайпов или перенаправлений).
Теперь вы можете перенаправить данные в stdin для R-скрипта:
$ cat datafile
1
2
4
$ ./mmmm.r < datafile
1
4
2
2.333333
Также работает для чисел с плавающей запятой:
$ cat datafile2
1.1
2.2
4.4
$ ./mmmm.r < datafile2
1.1
4.4
2.2
2.566667
Если вы не хотите писать файл R скрипта, вы можете использовать однострочник (с разрывами для удобства чтения) в командной строке с использованием Rscript
:
$ Rscript -e 'd<-scan("stdin", quiet=TRUE)' \
-e 'cat(min(d), max(d), median(d), mean(d), sep="\n")' < datafile
1
4
2
2.333333
Читайте обширные мануалы по R на http://cran.r-project.org/manuals.html.
К сожалению, полная справка доступна только в PDF. Другой способ ознакомиться со справкой — вбить ?topicname
в интерактивной R-сессии.
Для полноты картины: есть R-команда, которая выводит все значения, которые вам нужны, и даже больше. К сожалению, выводится в человеко-ориентированном формате, который сложно обработать программно.
> summary(c(1,2,4))
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.000 1.500 2.000 2.333 3.000 4.000
Я, на самом деле, держу небольшой awk-программу, чтобы получить сумму, количество данных, минимальный и максимальный элемент, среднее и медианное значение одного столбца числовых данных (включая отрицательные числа):
#!/bin/sh
sort -n | awk '
BEGIN {
c = 0;
sum = 0;
}
$1 ~ /^(\-)?[0-9]*(\.[0-9]*)?$/ {
a[c++] = $1;
sum += $1;
}
END {
ave = sum / c;
if( (c % 2) == 1 ) {
median = a[ int(c/2) ];
} else {
median = ( a[c/2] + a[c/2-1] ) / 2;
}
OFS="\t";
print sum, c, ave, median, a[0], a[c-1];
}
'
Вышеупомянутый скрипт читает из stdin и выводит таблично разделенные столбцы в одну строку.
Минимум:
jq -s min
awk 'NR==1||$0<x{x=$0}END{print x}'
Максимум:
jq -s max
awk 'NR==1||$0>x{x=$0}END{print x}'
Медиана:
jq -s 'sort|if length%2==1 then.[length/2|floor]else[.[length/2-1,length/2]]|add/2 end'
sort -n|awk '{a[NR]=$0}END{print(NR%2==1)?a[int(NR/2)+1]:(a[NR/2]+a[NR/2+1])/2}'
Среднее:
jq -s add/length
awk '{x+=$0}END{print x/NR}'
Объединено в одну команду (модифицировано из комментария):
$ seq 100|jq -s '{minimum:min,maximum:max,average:(add/length),median:(sort|if length%2==1 then.[length/2|floor]else[.[length/2-1,length/2]]|add/2 end)}'
{
"minimum": 1,
"maximum": 100,
"average": 50.5,
"median": 50.5
}
В jq
, опция -s
(--slurp
) создает массив для входных строк после их анализа как JSON, или в данном случае как число.
Или с помощью R (вы можете также использовать R -e
вместо Rscript -e
, но он выводит команды, которые выполняет, в STDOUT):
$ seq 100|Rscript -e 'summary(scan("stdin"))'
Read 100 items
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.00 25.75 50.50 50.50 75.25 100.00
$ seq 100|Rscript -e 'x=scan("stdin");sapply(c(min,max,mean,median),\(f)f(x))'
Read 100 items
[1] 1.0 100.0 50.5 50.5
$ seq 100|Rscript -e 'x=scan("stdin",quiet=T);writeLines(paste(sapply(c(min,max,mean,median),\(f)f(x)),collapse=" "))'
1 100 50.5 50.5
Минимум, максимум и среднее очень просто получить с помощью awk:
% echo -e '6\n2\n4\n3\n1' | awk 'NR == 1 { max=$1; min=$1; sum=0 }
{ if ($1>max) max=$1; if ($1<min) min=$1; sum+=$1;}
END {printf "Min: %d\tMax: %d\tAverage: %f\n", min, max, sum/NR}'
Min: 1 Max: 6 Average: 3,200000
Вычисление медианы несколько сложнее, так как нужно отсортировать числа и сохранить их все в памяти на некоторое время или прочитать их дважды (первый раз, чтобы их посчитать, второй — чтобы получить медианное значение). Вот пример, который сохраняет все числа в памяти:
% echo -e '6\n2\n4\n3\n1' | sort -n | awk '{arr[NR]=$1}
END { if (NR%2==1) print arr[(NR+1)/2]; else print (arr[NR/2]+arr[NR/2+1])/2}'
3
pythonpy отлично работает для таких вещей:
cat file.txt | py --ji -l 'min(l), max(l), numpy.median(l), numpy.mean(l)'
И Perl однострочник, включая медиану:
cat numbers.txt \
| perl -M'List::Util qw(sum max min)' -MPOSIX -0777 -a -ne 'printf "%-7s : %d\n"x4, "Min", min(@F), "Max", max(@F), "Average", sum(@F)/@F, "Median", sum( (sort {$a<=>$b} @F)[ int( $#F/2 ), ceil( $#F/2 ) ] )/2;'
Специальные опции, которые использовались:
-0777
: чтение всего файла сразу, а не построчно-a
: автоматическое разделение в массив @F
Более читаемая версия скрипта будет такой:
#!/usr/bin/perl
use List::Util qw(sum max min);
use POSIX;
@F=<>;
printf "%-7s : %d\n" x 4,
"Min", min(@F),
"Max", max(@F),
"Average", sum(@F)/@F,
"Median", sum( (sort {$a<=>$b} @F)[ int( $#F/2 ), ceil( $#F/2 ) ] )/2;
Если нужны десятичные, замените %d
на что-то вроде %.2f
.
nums=$(<file.txt);
list=(`for n in $nums; do printf "%015.06f\n" $n; done | sort -n`);
echo min ${list[0]};
echo max ${list[${#list[*]}-1]};
echo median ${list[${#list[*]}/2]};
Просто для представления различных вариантов на этой странице, вот еще два способа:
1: octave
- GNU Octave — это высокоуровневый интерпретируемый язык, в первую очередь предназначенный для численных вычислений. Он предоставляет возможности для численного решения линейных и нелинейных задач и выполнения других численных экспериментов.
Вот быстрый пример octave.
octave -q --eval 'A=1:10;
printf ("# %f\t%f\t%f\t%f\n", min(A), max(A), median(A), mean(A));'
# 1.000000 10.000000 5.500000 5.500000
2: bash + инструменты для отдельных целей.
Для работы bash с числами с плавающей точкой этот скрипт использует numprocess
и numaverage
из пакета num-utils
.
PS. Я также внимательно посмотрел на bc
, но для этой конкретной задачи он не предлагает ничего, кроме того, что делает awk
и этот bash скрипт…
arr=($(sort -n "LIST" |tee >(numaverage 2>/dev/null >stats.avg) ))
cnt=${#arr[@]}; ((cnt==0)) && { echo -e "0\t0\t0\t0\t0"; exit; }
mid=$((cnt/2));
if [[ ${cnt#${cnt%?}} == [02468] ]]
then med=$( echo -n "${arr[mid-1]}" |numprocess /+${arr[mid]},%2/ )
else med=${arr[mid]};
fi # count min max median average
echo -ne "$cnt\t${arr[0]}\t${arr[cnt-1]}\t$med\t"; cat stats.avg
Simple-r является ответом:
r summary file.txt
r -e 'min(d); max(d); median(d); mean(d)' file.txt
Он использует среду R для упрощения статистического анализа.
num
— это маленькая оболочка для awk
, которая делает это и даже больше, например:
$ echo "1 2 3 4 5 6 7 8 9" | num max
9
$ echo "1 2 3 4 5 6 7 8 9" | num min max median mean
..и так далее
она позволяет избежать изобретения велосипеда с использованием ultra-переносимого awk.
Документация приведена выше, а прямая ссылка здесь (также проверьтестраницу GitHub).
Я поддерживаю выбор lesmana в пользу R и предлагаю свою первую программу на R. Она читает по одному числу на строку из стандартного ввода и записывает четыре числа (min, max, average, median), разделенные пробелами, в стандартный вывод.
#!/usr/bin/env Rscript
a <- scan(file("stdin"), c(0), quiet=TRUE);
cat(min(a), max(a), mean(a), median(a), "\n");
С помощью perl
:
$ printf '%s\n' 1 2 4 |
perl -MList::Util=min,max -MStatistics::Basic=mean,median -le '
chomp(@l = <>); print for min(@l), max(@l), mean(@l), median(@l)'
1
4
2.33
2
Точность в функции Statistics::Basic
можно изменить либо с помощью переменной окружения $IPRES
, либо установив параметр ipres
в Statistics::Basic
; также помните, что разделительным знаком и десятичными знаками могут быть соответственно учтены локальные настройки:
$ printf '%s\n' 1 2 10004 |
LC_NUMERIC=fr_FR.UTF-8 perl -C -MList::Util=min,max \
-MStatistics::Basic=mean,median,ipres=6 -le '
chomp(@l = <>); print for min(@l), max(@l), mean(@l), median(@l)'
1
10004
3 335,666667
2
Расширяя ответ nisetama:
однострочник с jq
jq -s '{ min:min, max:max, sum:add, count:length, avg: (add/length), median: (sort|.[(length/2|floor)])
Пример:
echo 1 2 3 4 | jq -s '{ min:min, max:max, sum:add, count:length, avg: (add/length), median: (sort|.[(length/2|floor)]) }'
Возвращает:
{
"min": 1,
"max": 5,
"sum": 15,
"count": 5,
"avg": 3,
"median": 3
}
Примечание: Медиана немного некорректна при четном количестве элементов, но, на мой взгляд, близка.
Ниже приведена комбинация sort
/awk
, которая делает это:
sort -n | awk '{a[i++]=$0;s+=$0}END{print a[0],a[i-1],(a[int(i/2)]+a[int((i-1)/2)])/2,s/i}'
(она рассчитывает медиану как среднюю двух центральных значений, если количество значений четное)
Беря подсказки из кода Bruce, вот более эффективная реализация,
которая не держит все данные в памяти.
Как указано в вопросе,
она предполагает, что в файле входных данных (максимум) одно число на строке.
Она подсчитывает строки входного файла, содержащие подходящее число,
и передает количество в команду awk
,
предшествующую отсортированным данным.
Итак, например, если файл содержит
6.0
4.2
8.3
9.5
1.7
то ввод для awk
на самом деле выглядит так:
5
1.7
4.2
6.0
8.3
9.5
Затем скрипт awk
захватывает количество данных в блоке NR==1
и сохраняет среднее значение
(или два средних значения, которые усредняются для получения медианы)
когда видит их.
FILENAME="Salaries.csv"
(awk 'BEGIN {c=0} $1 ~ /^[-0-9]*(\.[0-9]*)?$/ {c=c+1;} END {print c;}' "$FILENAME"; \
sort -n "$FILENAME") | awk '
BEGIN {
c = 0
sum = 0
мед1_loc = 0
мед2_loc = 0
мед1_val = 0
мед2_val = 0
мин = 0
макс = 0
}
NR==1 {
LINES = $1
# Мы проверяем, является ли количество строк четным или нечетным, чтобы сохранять только
# места в массиве, где может находиться медиана.
если (LINES%2==0) {med1_loc = LINES/2-1; med2_loc = med1_loc+1;}
если (LINES%2!=0) {med1_loc = med2_loc = (LINES-1)/2;}
}
$1 ~ /^[-0-9]*(\.[0-9]*)?$/ && NR!=1 {
# установка минимального значения
если (c==0) {min = $1;}
# средние два значения в массиве
если (c==med1_loc) {med1_val = $1;}
если (c==med2_loc) {med2_val = $1;}
c++
sum += $1
max = $1
}
END {
ave = sum / c
median = (med1_val + med2_val ) / 2
print "sum:" sum
print "count:" c
print "mean:" ave
print "median:" median
print "min:" min
print "max:" max
}
'
С R однострочником:
R -q -e 'summary(as.numeric(read.table("your_single_col_file")[,1]))'
Например, для моего файла, я получил такой вывод:
Min. 1st Qu. Median Mean 3rd Qu. Max.
550.4 628.3 733.1 706.5 778.4 832.9
cat/python
только решение – незащищенное от пустого ввода!
cat data | python3 -c "import fileinput as FI,statistics as STAT; i = [float(l.strip()) for l in FI.input()]; print('min:', min(i), ' max: ', max(i), ' avg: ', STAT.mean(i), ' median: ', STAT.median(i))"
function median()
{
declare -a nums=($(cat))
printf '%s\n' "${nums[@]}" | sort -n | tail -n $((${#nums[@]} / 2 + 1)) | head -n 1
}
Если вам нужен больше утилитарный подход, а не крутость или хитроумие, то perl
скорее подойдет, чем awk
. В общем, он будет на каждой *nix с постоянным поведением, и его легко и бесплатно установить на Windows.
Я думаю, что он также менее криптован, чем awk
, и там могут быть статистические модули, которые вы могли бы использовать, если хотите нечто среднее между написанием собственных групп и чем-то подобным R.
Мой довольно непроверенный (на самом деле я знаю, что в нем есть ошибки, но он работает для моих целей) сценарий на perl
занял примерно минуту для написания, и я полагаю, что единственным неочевидным моментом является while(<>)
, что является очень полезным сокращением, означающим взять файл(ы), переданные в виде аргументов командной строки, прочитать построчно и поместить эту строку в специальную переменную $_
.
Таким образом вы можете поместить это в файл, названный count.pl и запустить его как perl count.pl myfile
.
Помимо этого, должно быть больно очевидно, что происходит.
$max = 0;
while (<>) {
$sum = $sum + $_;
$max = $_ if ($_ > $max);
$count++;
}
$avg=$sum/$count;
print "$count numbers total=$sum max=$max mean=$avg\n";
Я написал скрипт ‘stats’ на perl, который делает это и ещё больше.
(& вы можете выбрать только нужные части, используя такие опции, как ‘–sum’ ‘–median’, и т.д.)
$ ls -lR | grep $USER| scut -f=4 | stats
Sum 1.22435e+08
Number 428
Mean 286064
Median 4135
Mode 0
NModes 4
Min 0
Max 8.47087e+07
Range 8.47087e+07
Variance 1.69384e+13
Std_Dev 4.11563e+06
SEM 198936
95% Conf -103852 to 675979
[для нормального распределения (ND) - смещение]
Quantiles (5)
Index Value
1 85 659
2 171 2196
3 256 11015
4 342 40210
Сдвиг 20.3201
[Сдвиг=0 для симметричного распределения]
Стд_Сдвиг 171.621
Куртозис 413.679
[Куртозис=3 для ND]
ПопКурт 0.975426
[Популяционный куртозис нормирован на размер выборки; PK=0 для ND]
Он поставляется с scut (что-то вроде perl-жесткого cut/join) на:
https://github.com/hjmangalam/scut
Если ваш список чисел короткий и вам не нужен результат программно, стоит отметить, что иногда наилучший шаг — это преобразовать колонку чисел в массив:
tr '\n' ',' | awk '{printf("a = [%s]\n", $1)}'
Затем вставьте это в ваш интерпретатор на выбор, например, интерпретатор Python, и вы можете вычислить min/max/mean/median/mode и так далее, как вам нужно.
Попробуйте csvstat
или xsv stats
Основные наборы инструментов CSV csvkit
и xsv
включают некоторые функции базовой статистики.
Так что просто представьте, что ваши данные, содержащие одну запись на строку, являются одним столбцом CSV файла без заголовка.
CSVKIT старый и более известный, поэтому вы можете обычно легко установить его через ваш предпочитаемый менеджер пакетов. XSV новее и намного быстрее для больших данных, но возможно, вам придется установить его вручную.
Вход:
$ echo 1 2 9 9 | tr " " "\n"
1
2
9
9
csvkit’s csvstat
csvstat
— одна из команд csvkit.
По умолчанию, вывод csvstat предназначен для восприятия человеком…
$ echo 1 2 9 9 | tr " " "\n" | csvstat --no-header-row
/usr/lib/python2.7/site-packages/agate/table/from_csv.py:74: RuntimeWarning: Error sniffing CSV dialect: Could not determine delimiter
1. "a"
Тип данных: Number
Содержит нулевые значения: False
Уникальные значения: 3
Наименьшее значение: 1
Наибольшее значение: 9
Сумма: 21
Среднее: 5.25
Медиана: 5.5
StDev: 4.349
Наиболее частые значения: 9 (2x)
1 (1x)
2 (1x)
Количество строк: 4
…но вы также можете получить вывод в виде самого CSV, что будет лучше для дальнейшей обработки:
$ echo 1 2 9 9 | tr " " "\n" | csvstat --no-header-row --csv
/usr/lib/python2.7/site-packages/agate/table/from_csv.py:74: RuntimeWarning: Error sniffing CSV dialect: Could not determine delimiter
column_id,column_name,type,nulls,unique,min,max,sum,mean,median,stdev,len,freq
1,a,Number,False,3,1,9,21,5.25,5.5,4.349,,"9, 1, 2"
csvstat всегда жалуется на отсутствие разделителя в строках. Чтобы избавиться от этого сообщения об ошибке, просто перенаправьте его в /dev/null как показано ниже:
$ echo 1 2 9 9 | tr " " "\n" | csvstat --no-header-row --csv 2>/dev/null
column_id,column_name,type,nulls,unique,min,max,sum,mean,median,stdev,len,freq
1,a,Number,False,3,1,9,21,5.25,5.5,4.349,,"9, 1, 2"
А если вы хотите немного более читаемую версию для человека, вы можете пропустить всё это через csvlook
снова:
$ echo 1 2 9 9 | tr " " "\n" | csvstat --no-header-row --csv 2>/dev/null | csvlook
| column_id | column_name | type | nulls | unique | min | max | sum | mean | median | stdev | len | freq |
| --------- | ----------- | ------ | ----- | ------ | ---- | --- | --- | ---- | ------ | ----- | --- | ------- |
| True | a | Number | False | 3 | True | 9 | 21 | 5.25 | 5.5 | 4.349 | | 9, 1, 2 |
xsv stats
Из соображений скорости xsv stats
не включает медиану по умолчанию…
$ echo 1 2 9 9 | tr " " "\n" | xsv stats --no-headers
field,type,sum,min,max,min_length,max_length,mean,stddev
0,Integer,21,1,9,1,1,5.25,3.766629793329841
$ echo 1 2 9 9 | tr " " "\n" | xsv stats --no-headers | xsv table
field type sum min max min_length max_length mean stddev
0 Integer 21 1 9 1 1 5.25 3.766629793329841
…но вы можете включить её, использовав переключатель --everything
. Это добавит три дополнительных столбца: median,mode,cardinality
:
$ echo 1 2 9 9 | tr " " "\n" | xsv stats --no-headers --everything
field,type,sum,min,max,min_length,max_length,mean,stddev,median,mode,cardinality
0,Integer,21,1,9,1,1,5.25,3.766629793329841,5.5,9,3
$ echo 1 2 9 9 | tr " " "\n" | xsv stats --no-headers --everything | xsv table
field type sum min max min_length max_length mean stddev median mode cardinality
0 Integer 21 1 9 1 1 5.25 3.766629793329841 5.5 9 3
Примечание о вещественных числах
К вашему сведению: нецелые числа, похоже, обрабатываются по-разному в csvkit и xsv:
$ echo 1.1 2.2 9.9 9.9 | tr " " "\n" | csvstat --no-header-row --csv 2>/dev/null | csvcut -c median
median
6.05
$ echo 1.1 2.2 9.9 9.9 | tr " " "\n" | xsv stats --no-headers --everything | xsv select median
median
6.050000000000001
.
Ответ или решение
Чтобы получить минимальное, максимальное, медиальное и среднее значение списка чисел в bash-скрипте с помощью одной команды, существует несколько подходов. Рассмотрим несколько способов, включая использование утилит GNU datamash, R, awk, и jq, поскольку они являются наиболее подходящими инструментами для выполнения такой задачи.
Использование GNU datamash
GNU datamash предоставляет удобный способ обработки числовых данных. Вы можете использовать эту утилиту, чтобы получить необходимые статистические метрики:
printf '%s\n' $(cat ваш_файл.txt) | datamash min 1 max 1 mean 1 median 1
С использованием R
R — это мощный инструмент для статистической обработки данных. Вот как использовать его для получения необходимых значений:
- Создайте R скрипт, например mmmm.r:
#!/usr/bin/env Rscript
d <- scan("stdin", quiet=TRUE)
cat(min(d), max(d), median(d), mean(d), sep="\n")
- Затем выполните скрипт, передавая данные из файла:
./mmmm.r < ваш_файл.txt
Использование awk
Awk — это мощный текстовый процессор, который может быть использован для выполнения всех этих операций, хотя расчет медианы требует дополнительных усилий:
cat ваш_файл.txt | sort -n | awk '
NR == 1 { min = $1; }
{ arr[NR] = $1; sum += $1; }
END {
max = arr[NR];
mean = sum / NR;
if (NR % 2) { median = arr[(NR + 1) / 2]; }
else { median = (arr[NR / 2] + arr[NR / 2 + 1]) / 2; }
print "Min:", min, "Max:", max, "Mean:", mean, "Median:", median;
}'
Использование jq
Jq — это инструмент для обработки JSON, но его также можно использовать для обработки числовых данных:
jq -s '{min: min, max: max, mean: (add/length), median: (sort | if length % 2 == 1 then .[length/2 | floor] else ([.[length/2-1], .[length/2]] | add/2) end)}' < ваш_файл.txt
Заключение
Каждый из предложенных методов имеет свои преимущества и недостатки. Выбор конкретного зависит от ваших предпочтений и доступных инструментов. GNU datamash и R являются мощными инструментами для обработки чисел и статистических расчетов, тогда как awk и jq предоставляют более гибкие текстовые процессы. Выбирать стоит исходя из ваших конкретных нужд и окружения.