Существует ли способ получить минимальное, максимальное, медиану и среднее значение списка чисел с помощью одной команды?

Вопрос или проблема

У меня есть список чисел в файле, одно на строку. Как я могу получить минимальное, максимальное, медианное и среднее значения? Я хочу использовать результаты в 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 — это мощный инструмент для статистической обработки данных. Вот как использовать его для получения необходимых значений:

  1. Создайте R скрипт, например mmmm.r:
#!/usr/bin/env Rscript
d <- scan("stdin", quiet=TRUE)
cat(min(d), max(d), median(d), mean(d), sep="\n")
  1. Затем выполните скрипт, передавая данные из файла:
./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 предоставляют более гибкие текстовые процессы. Выбирать стоит исходя из ваших конкретных нужд и окружения.

Оцените материал
Добавить комментарий

Капча загружается...