【问题标题】:Awk to compute average ignoring outliers -for a segmented file用于计算平均忽略异常值的 Awk - 对于分段文件
【发布时间】:2014-09-19 07:37:51
【问题描述】:

我有如下所示的数据文件 (data.txt),

0.01667 20.53
0.01667 6.35
0.01667 6.94
0.01667 7.07
0.01667 8.06
0.01667 8.10
0.01667 8.25
0.01667 8.71
0.01667 9.31
0.02500 20.19
0.02500 6.35
0.02500 6.92
0.02500 7.07
0.02500 8.08
0.02500 8.09
0.02500 8.24
0.02500 8.70
0.02500 9.26
0.03333 19.89
0.03333 6.33
0.03333 6.90
0.03333 7.07
0.03333 8.07
0.03333 8.09
0.03333 8.22
0.03333 8.70
0.03333 9.22
0.04167 19.65
0.04167 6.34
0.04167 6.87
0.04167 7.07
0.04167 8.03
0.04167 8.08
0.04167 8.19
0.04167 8.69
0.04167 9.19
0.05000 19.40
0.05000 6.32
0.05000 6.85
0.05000 7.06
0.05000 8.02
0.05000 8.09
0.05000 8.16
0.05000 8.71
0.05000 9.15
0.05833 19.12
0.05833 6.29
0.05833 6.84
0.05833 7.04
0.05833 8.01
0.05833 8.11
0.05833 8.16
0.05833 8.71
0.05833 9.11
0.06667 18.84
0.06667 6.29
0.06667 6.82
0.06667 7.05
0.06667 7.98
0.06667 8.11
0.06667 8.14
0.06667 8.71
0.06667 9.06
0.07500 18.57
0.07500 6.29
0.07500 6.80
0.07500 7.06
0.07500 7.97
0.07500 8.10
0.07500 8.13
0.07500 8.71
0.07500 9.02

第 1 列是第 2 列中进行测量的时间。我需要对第 1 列中给出的每个时间的第 2 列中的值进行平均,并输出该时间的值和该时间的平均值。我可以使用以下 awk 代码进行平均

awk '{if($1<0)$1=0}
    {
        sum[$1]+=$2
        cnt[$1]++
    }
    END {
    #     print "Name" "\t" "sum" "\t" "cnt" "\t" "avg"
        for (i in sum)
            printf "%8.5f   %6.2f   %6d   %6.3f\n", i, sum[i], cnt[i], sum[i]/cnt[i]

    }' data.txt  | sort -n -k1 > avgFile.txt

请注意,我还输出了一些其他内容,以便我可以检查是否在做正确的事情。正如您所看到的,每个时隙的数据都包含异常值,我需要删除这些。我尝试将在 0.01667 收集的数据选择到某个文件 temp.txt,并且我有以下 awk 代码可以正确删除异常值

awk 'BEGIN{CNT=0} {ROW[CNT]=$0;DATA[CNT]=$2; 
    TOTAL+=$2;CNT+=1;} END{for (i = 0;i < NR; i++){if ((sqrt((DATA[i]-(TOTAL/NR))^2))<((TOTAL/NR)*30/100)) 
    {print ROW[i] ;}}}' temp.txt

但我需要在原始代码中执行此操作,以便在计算第 2 列中的值的平均值之前,在每次有一个异常值时删除该异常值

我们将不胜感激。

【问题讨论】:

  • 您的问题不在于 awk,而在于您需要在读取数据时检测异常值。你会怎么做?将第一个点检测为异常值?这不是微不足道的,参见例如citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.109.1943.
  • 感谢您的评论本杰明,在这种情况下,我使用了标准偏差,我认为数据中的一个值与平均值的偏差超过 30%,这是一个异常值,我认为这是有足够的理由从平均计算中删除这样的值。感谢研究的链接,我还不能下载完整的手稿。一旦我有完整的论文,我会阅读它
  • 您可以尝试一些简单的方法。就像将特定时间的所有值读入一个数组,对数据进行平均,然后计算每个点的偏差,删除任何大于您提出的某个阈值的点。例如.01667,平均值为9.25,如果您将阈值设置为4,您将消除异常值。同样的逻辑似乎适用于所有异常值。
  • 嗨,大卫,我一直在尝试做类似的事情,正如您在我的第二段代码中看到的那样,我使用的事实是,如果特定值与平均值的偏差超过 30% ,我删除它。我现在面临的问题是如何在我的代码中实现这一点,我既选择特定时间的数据,然后删除异常值,最后计算平均值。我的 awk 技能还处于起步阶段
  • @malandisa 我不是awk 专家,但是一个简单的 bash 脚本可以轻松处理它。只需使用while loop 并将值同时添加到 tmp 数组,处理数组以删除异常值,然后获取剩余值的 avg、mean 和 std dev,取消设置数组并移至下一次。如果有机会,我会写一个例子。

标签: bash awk average outliers


【解决方案1】:

这会计算平均值,然后删除异常值,然后在删除异常值后重新计算平均值:

$ cat tst.awk
{
    vals[$1][$2]
    sum[$1] += $2
    cnt[$1]++
}

END {
    div = 0.3
    for (time in vals) {
        ave  = sum[time] / cnt[time]
        low  = ave * (1 - div)
        high = ave * (1 + div)
        for (val in vals[time]) {
            if ( (val < low) || (val > high) ) {
                print "Deleting outlier", time, val | "cat>&2"
                sum[time] -= val
                cnt[time]--
            }
        }
    }

    for (time in vals) {
        ave = (cnt[time] > 0 ? sum[time] / cnt[time] : 0)
        print time, sum[time], cnt[time], ave
    }
}

.

$ awk -f tst.awk file
0.05000 56.04 7 8.00571
0.07500 62.08 8 7.76
0.04167 56.12 7 8.01714
0.03333 56.27 7 8.03857
0.01667 56.44 7 8.06286
0.06667 55.87 7 7.98143
0.02500 56.36 7 8.05143
0.05833 55.98 7 7.99714
Deleting outlier 0.05000 6.32
Deleting outlier 0.05000 19.40
Deleting outlier 0.07500 18.57
Deleting outlier 0.04167 19.65
Deleting outlier 0.04167 6.34
Deleting outlier 0.03333 6.33
Deleting outlier 0.03333 19.89
Deleting outlier 0.01667 6.35
Deleting outlier 0.01667 20.53
Deleting outlier 0.06667 6.29
Deleting outlier 0.06667 18.84
Deleting outlier 0.02500 20.19
Deleting outlier 0.02500 6.35
Deleting outlier 0.05833 6.29
Deleting outlier 0.05833 19.12

这就是你要找的吗?它将 GNU awk 用于真正的二维数组。

【讨论】:

  • 艾德!!谢谢!这完美地工作。事实上,这正是我所需要的,我从中学到了很多。谢谢
  • 不客气,如果这是您想要的答案,请点击旁边的复选标记。
  • 我真的需要更多地研究 awk。这要简单得多。但是,我无法弄清楚你在哪里显示没有异常值的平均值?也许我只是误读了这个问题。就像时间0.01667 我得到一个没有7.84875 异常值的平均值。手动计算也一样?
  • print time, sum[time], cnt[time], ave 正在打印由sum[time] -= val 删除异常值并且值计数由cnt[time]-- 递减后的平均值。我的计算发现 0.01667 有 2 个异常值(6.3520.53,因为所有值的原始平均值是 9.25778 所以这两个值分别小于该值的 70% 和大于 130%)所以如果你计算只找到一个,然后就可以解释为什么最终结果不同。 OP 没有说明识别异常值的算法是什么,所以我猜测了一下。
  • 虽然if a particular value has more than 30% divergence from the average, I remove it. OP 确实在评论中说,所以当我在计算中使用 30% 时 (div = 0.3) 我并没有凭空取出它,我只是不确定如果我使用它的方式正是他们的想法。
【解决方案2】:

好的,我告诉过你,当我有时间时,我会编写一个快速脚本(结果不是那么快)这会删除异常值并返回清理后的数组的平均值。如果需要,您可以实施标准偏差。如果您有任何问题,请告诉我。:

#!/bin/bash

## generic error/usage function
function usage {
    local ecode=${2:-0}
    test -n "$1" && printf "\n %s\n" "$1" >&2
cat >&2 << helpMessage

usage:  ${0//*\//} datafile

This script will process a 2-column datafile to provide average, 
mean and std. deviation for each time group of data while removing
outlying data from the calculation. The datafile format:

    time    value
    0.01667 20.53  <- outlier
    0.01667 6.35
    0.01667 6.94
    ...

Options:

    -h  |  --help  program help (this file)

helpMessage

    exit $ecode;
}

## function to calculate average of arguments
function average {

    local sum=0
    declare -i count=0
    for n in $@; do
        sum=$( printf "scale=6; %s+%s\n" "$sum" "$n" | bc )
        ((count++))
    done
    avg=$( printf "scale=6; %s/%s\n" "$sum" "$count" | bc )
    printf "%s\n" "$avg"
}

## function to examine arguments a remove any outlier
#  that is greater than 4 from the average.
#  values without the outlier are returned to command line
function rmoutlier {

    local avg=$(average $@)
    local diff=0
    for i in $@; do
        diff=$( printf "scale=6; %s-%s\n" "$i" "$avg" | bc )
        [ "${diff:0:1}" = '-' ]  && diff="${diff:1}"            # quick absolute value hack
        [ "${diff:0:1}" = '.' ]  && diff=0                      # set any fractional 0
        if [ $((${diff//.*/})) -lt 4 ]; then
            clean+=( $i )                                       # if whole num diff < 4, keep
        else
            echo "->outlier: $i" >&2                            # print outlier to stderr
        fi
    done
    echo ${clean[@]}                                            # return array
}

## respond to -h or --help
test "${1:1}" = 'h' || test "${1:2}" = 'help' && usage

## set variables
dfn="${1:-dat/outlier.dat}"     # datafile (default dat/outlier.dat)
declare -a tmp                  # temporary array holding data for given time
ptime=0                         # variable holding previous time (flag for 1st line)

## validate input filename
test -r "$dfn" || usage "Error: invalid input. File '$dfn' not found" 1

while read -r time data || [ -n "$data" ]; do               # read all lines of data

    if [ "$ptime" = 0 ] || [ "$ptime" = "$time" ]; then     # if no change in time
        tmp+=( $data )                                      # fill array with data
    else
        echo "  time: $ptime  data : '${tmp[@]}'" >&2       # output array to stderr

        ## process data
        clean=( $(rmoutlier ${tmp[@]} ) )                   # remove outlier
        echo  "  time: $ptime  clean: '${clean[@]}'" >&2    # output clean array
        avgclean=$( average ${clean[@]} )                   # average clean array
        printf "  avgclean: %s\n\n" "$avgclean" >&2         # output avg of clean array

        unset tmp           # reset variables for next time
        unset clean
        unset avgclean

        tmp+=( $data )      # read first value for next time set
    fi

    ptime="$time"           # save previous time for comparison

done <"$dfn"

## process final time block

echo "  time: $ptime  data : '${tmp[@]}'" >&2

## process data
clean=( $(rmoutlier ${tmp[@]} ) )
echo  "  time: $ptime  clean: '${clean[@]}'" >&2
avgclean=$( average ${clean[@]} )
printf "  avgclean: %s\n\n" "$avgclean" >&2

unset tmp
unset clean
unset avgclean

exit 0

用法:

./outlier.sh  datafile

输出:

$ ./outlier.sh dat/outlier.dat
  time: 0.01667  data : '20.53 6.35 6.94 7.07 8.06 8.10 8.25 8.71 9.31'
->outlier: 20.53
  time: 0.01667  clean: '6.35 6.94 7.07 8.06 8.10 8.25 8.71 9.31'
  avgclean: 7.848750

  time: 0.02500  data : '20.19 6.35 6.92 7.07 8.08 8.09 8.24 8.70 9.26'
->outlier: 20.19
  time: 0.02500  clean: '6.35 6.92 7.07 8.08 8.09 8.24 8.70 9.26'
  avgclean: 7.838750

  time: 0.03333  data : '19.89 6.33 6.90 7.07 8.07 8.09 8.22 8.70 9.22'
->outlier: 19.89
  time: 0.03333  clean: '6.33 6.90 7.07 8.07 8.09 8.22 8.70 9.22'
  avgclean: 7.825000

  time: 0.04167  data : '19.65 6.34 6.87 7.07 8.03 8.08 8.19 8.69 9.19'
->outlier: 19.65
  time: 0.04167  clean: '6.34 6.87 7.07 8.03 8.08 8.19 8.69 9.19'
  avgclean: 7.807500

  time: 0.05000  data : '19.40 6.32 6.85 7.06 8.02 8.09 8.16 8.71 9.15'
->outlier: 19.40
  time: 0.05000  clean: '6.32 6.85 7.06 8.02 8.09 8.16 8.71 9.15'
  avgclean: 7.795000

  time: 0.05833  data : '19.12 6.29 6.84 7.04 8.01 8.11 8.16 8.71 9.11'
->outlier: 19.12
  time: 0.05833  clean: '6.29 6.84 7.04 8.01 8.11 8.16 8.71 9.11'
  avgclean: 7.783750

  time: 0.06667  data : '18.84 6.29 6.82 7.05 7.98 8.11 8.14 8.71 9.06'
->outlier: 18.84
  time: 0.06667  clean: '6.29 6.82 7.05 7.98 8.11 8.14 8.71 9.06'
  avgclean: 7.770000

  time: 0.07500  data : '18.57 6.29 6.80 7.06 7.97 8.10 8.13 8.71 9.02'
->outlier: 18.57
  time: 0.07500  clean: '6.29 6.80 7.06 7.97 8.10 8.13 8.71 9.02'
  avgclean: 7.760000

附录:将时间和平均值写入文件

下面是脚本的更新部分,它将把timeclean average 输出到一个文件(默认值:dat/outlier.out)。只有包含“输出文件名”ofn 的行发生了变化。 (你可以将任何你想要的输出文件名作为第二个参数传递给脚本)所以新的usage: 将是:outlier.sh input_file output_file

## set variables
dfn="${1:-dat/outlier.dat}"     # datafile (default dat/outlier.dat)
ofn="${2:-dat/outlier.out}"     # output file (default dat/outlier.out)
declare -a tmp                  # temporary array holding data for given time
ptime=0                         # variable holding previous time (flag for 1st line)

:> "$ofn"                       # truncate output file

## validate input filename
test -r "$dfn" || usage "Error: invalid input. File '$dfn' not found" 1

while read -r time data || [ -n "$data" ]; do               # read all lines of data

    if [ "$ptime" = 0 ] || [ "$ptime" = "$time" ]; then     # if no change in time
        tmp+=( $data )                                      # fill array with data
    else
        echo "  time: $ptime  data : '${tmp[@]}'" >&2       # output array to stderr
        printf "  time: %s  " "$ptime" >>"$ofn"             # output array to file

        ## process data
        clean=( $(rmoutlier ${tmp[@]} ) )                   # remove outlier
        echo  "time: $ptime  clean: '${clean[@]}'" >&2      # output clean array
        avgclean=$( average ${clean[@]} )                   # average clean array
        printf "  avgclean: %s\n\n" "$avgclean" >&2         # output avg of clean array
        printf "  avgclean: %s\n" "$avgclean" >>"$ofn"      # output avg of clean array to file

        unset tmp           # reset variables for next time
        unset clean
        unset avgclean

        tmp+=( $data )      # read first value for next time set
    fi

    ptime="$time"           # save previous time for comparison

done <"$dfn"

outlier.out:

time: 0.01667    avgclean: 7.848750
time: 0.02500    avgclean: 7.838750
time: 0.03333    avgclean: 7.825000
time: 0.04167    avgclean: 7.807500
time: 0.05000    avgclean: 7.795000
time: 0.05833    avgclean: 7.783750
time: 0.06667    avgclean: 7.770000

【讨论】:

  • 大卫,谢谢你的剧本。现在我想弄清楚如何将时间和该时间的平均值输出到文件中,而不是将所有内容都打印到屏幕上。这对我学习 bash 脚本很有帮助,但我花了很长时间,但仍然无法正确完成。
  • 谢谢大卫。现在它确实输出到文件了。唯一的缺点是它很慢。对于大约 1010 行的文件,处理大约需要 2 分钟,这非常慢。有没有办法让速度更快或者这是最好的?请对此文件进行测试...dropbox.com/s/1scfvhz6aom3jvj/Data.txt?dl=0
  • -24.000 应该以某种方式转换为 0.00。
猜你喜欢
  • 2020-03-28
  • 2016-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多