unix bash – Manera eficiente de hacer cálculos en bash

Pregunta:

Estoy tratando de calcular la media geométrica de un archivo lleno de números (1 columna).

La fórmula básica para la media geométrica es el promedio del logaritmo natural (o logaritmo) de todos los valores y luego elevar e (o base 10) a ese valor.

Mi script actual de bash solo se ve así:

# Geometric Mean
count=0;
total=0; 

for i in $( awk '{ print $1; }' input.txt )
  do
    if (( $(echo " "$i" > "0" " | bc -l) )); then
        total="$(echo " "$total" + l("$i") " | bc -l )"
        ((count++))
    else
      total="$total"
    fi
  done

Geometric_Mean="$( printf "%.2f" "$(echo "scale=3; e( "$total" / "$count" )" | bc -l )" )"
echo "$Geometric_Mean"

Esencialmente:

  1. Verifique cada entrada en el archivo de entrada para asegurarse de que sea mayor que 0 llamando a bc cada vez
  2. Si la entrada es> 0, tomo el registro natural (l) de ese valor y lo agrego al total acumulado de llamadas bc cada vez
  3. Si la entrada es <= 0, no hago nada
  4. Calcular la media geométrica

Esto funciona perfectamente bien para un pequeño conjunto de datos. Desafortunadamente, estoy tratando de usar esto en un gran conjunto de datos (input.txt tiene 250,000 valores). Si bien creo que esto eventualmente funcionará, es extremadamente lento. Nunca he tenido la paciencia suficiente para dejar que termine (más de 45 minutos).

Necesito una forma de procesar este archivo de manera más eficiente.

Hay formas alternativas como usar Python

# Import the library you need for math
import numpy as np

# Open the file
# Load the lines into a list of float objects
# Close the file
infile = open('time_trial.txt', 'r')
x = [float(line) for line in infile.readlines()]
infile.close()

# Define a function called geo_mean
# Use numpy create a variable "a" with the ln of all the values
# Use numpy to EXP() the sum of all of a and divide it by the count of a
# Note ... this will break if you have values <=0
def geo_mean(x):
    a = np.log(x)
    return np.exp(a.sum()/len(a))

print("The Geometric Mean is: ", geo_mean(x))

Me gustaría evitar el uso de Python, Ruby, Perl … etc.

¿Alguna sugerencia sobre cómo escribir mi script bash de manera más eficiente?

Respuesta:

Por favor, no hagas esto en el caparazón. No hay cantidad de ajustes que lo hagan remotamente eficiente. Los bucles de shell son lentos y usar el shell para analizar el texto es una mala práctica. Todo su script puede ser reemplazado por este simple awk one-line que será órdenes de magnitud más rápido:

awk 'BEGIN{E = exp(1);} $1>0{tot+=log($1); c++} END{m=tot/c; printf "%.2f\n", E^m}' file

Por ejemplo, si ejecuto eso en un archivo que contiene los números del 1 al 100, obtengo:

$ seq 100 > file
$ awk 'BEGIN{E = exp(1);} $1>0{tot+=log($1); c++} END{m=tot/c; printf "%.2f\n", E^m}' file
37.99

En términos de velocidad, probé su solución de shell, su solución de python y el awk que di arriba en un archivo que contiene los números del 1 al 10000:

## Shell
$ time foo.sh
3677.54

real    1m0.720s
user    0m48.720s
sys     0m24.733s

### Python
$ time foo.py
The Geometric Mean is:  3680.827182220091

real    0m0.149s
user    0m0.121s
sys     0m0.027s


### Awk
$ time awk 'BEGIN{E = exp(1);} $1>0{tot+=log($1); c++} END{m=tot/c; printf "%.2f\n", E^m}' input.txt
3680.83

real    0m0.011s
user    0m0.010s
sys     0m0.001s

Como puede ver, el awk es incluso más rápido que el python y mucho más sencillo de escribir. También puede convertirlo en un script de "shell", si lo desea. O así:

#!/bin/awk -f

BEGIN{
    E = exp(1);
} 
$1>0{
    tot+=log($1);
    c++;
}
 
END{
    m=tot/c; printf "%.2f\n", E^m
}

o guardando el comando en un script de shell:

#!/bin/sh
awk 'BEGIN{E = exp(1);} $1>0{tot+=log($1); c++;} END{m=tot/c; printf "%.2f\n", E^m}' "$1"

Leave a Comment

Your email address will not be published.

Scroll to Top

istanbul avukat

-

web tasarım