Melhor método para coletar uma amostra aleatória de uma coleção de arquivos


23

Suponha que exista um diretório contendo 300 arquivos de dados. Quero selecionar aleatoriamente 200 desses arquivos e movê-los para outro diretório. Existe uma maneira de fazer isso no Unix / Linux?


R provavelmente pode fazer isso em um piscar de um olho com list.files()...
— sr_

4
Eu vagamente ligar juntos shufe head(ou o uso apenas shuf -n, deve ter lido a página man ...)
— Ulrich Schwarz

Respostas:


32

Se o seu sistema possui shuf, você pode usá-lo de maneira bastante conveniente (mesmo lidando com nomes de arquivos feios):

shuf -zen200 source/* | xargs -0 mv -t dest

Se você não tem, shufmas tem um sortque leva -R, isso deve funcionar:

find source -type f -print0 | sort -Rz | cut -d $'\0' -f-200 | xargs -0 mv -t dest

7
Ah, sim, porque onde mais alguém procuraria embaralhar do que em uma ferramenta de classificação. (Pelo menos shufnão é chamado trosporque ele faz o oposto de classificação.)
— Ulrich Schwarz

2
Não existe o oposto de classificar (no mesmo sentido que não existe "sem clima"). Random ainda está classificado, apenas classificado aleatoriamente.
— Plutor

1
O que é o "-zen200"? Isso não está na documentação do shuf ou em qualquer lugar da Internet, mas seu exemplo não funciona sem ele. Bastante místico.
— SigmaX

2
@SigmaX Na verdade, é bem zen, não é? Dica: são 3 bandeiras separadas.
— 21715 Kevin

2
files=(*)
for (( i=0; i<200; i++ )); do
    keys=("${!files[@]}")
    rnd=$(( RANDOM % ${#keys[@]} ))
    key=${keys[$rnd]}
    mv "${files[$key]}" "$otherdir"
    unset files[$key]
done

2

Coloque todos os nomes de arquivos em uma matriz chamada "arquivos" no bash:

files=( * )

tamanho da matriz:

echo ${#files[@]}

defina 2/3 deles como tamanho da amostra:

take=$((2*${#files[@]}/3)) 

for i in $(seq 1 $take)
do
    r=$((RANDOM%${#files[@]})) 
    echo ${files[r]}
done

Isso selecionará duplicatas e não será testado com nomes de arquivos com espaços em branco e outros.

A maneira mais simples de evitar duplicatas é iterar sobre todos os arquivos e escolher cada um com chance de 2/3, mas isso não leva necessariamente a 200 arquivos.

Isso removerá um arquivo se ele tiver sido escolhido da lista e atender aos seus requisitos:

#!/bin/bash
files=( * )
# define 2/3 of them as sample size:
take=$((2*${#files[@]}/3)) 

while (( i < $take ))
do
    r=$((RANDOM%${#files[@]})) 
    f=${files[r]}
    if [[ -n $f ]]
    then 
        i=$((i+1))    
        echo ${files[r]}
        unset files[r]    
    fi
done

Você pode selecionar o mesmo arquivo mais de uma vez.
— Glenn Jackman

Script shell muito bom. Para contornar o problema de não obter 200 arquivos, você provavelmente deseja usar o Reservoir Sampling: en.wikipedia.org/wiki/Reservoir_sampling Vou ser fraco e não incluir um exemplo de script de shell.
— 11558 Bruce EdigerBom

@glennjackman: Eu escrevi, sim. Foram necessários alguns minutos para descobrir como remover entradas da matriz.
— usuário desconhecido

Advertência secundária: $RANDOMpode ter apenas valores de 0 a 32767; portanto, isso não funcionará corretamente se você tiver mais de 32768 arquivos. Além disso, a busca é influenciada pelos primeiros arquivos.
— L0b0

@ l0b0: Requisitos onde, para escolher 200 entre 300. Se os arquivos não estiverem no diretório atual, mas em um servidor de arquivos, ele também não funcionará. Requisitos diferentes, resposta diferente.
— usuário desconhecido

2

Se isso precisar ser estatisticamente aleatório, você não deve usá-lo RANDOM % ${#keys[@]}. Considerar:

  1. $RANDOM tem 32768 valores únicos
  2. A primeira seleção é 1 de 300 elementos
  3. 32768 = 109 * 300 + 68

Portanto, ao selecionar o primeiro item, há uma chance de 110/32768 ~ = 0,333569% para cada um dos 68 primeiros elementos e 109/32768 ~ = 0,33264% de chance para cada um dos outros 232 elementos a serem selecionados. A escolha é repetida várias vezes com chances diferentes, mas inclinado para os primeiros elementos sempre que 32768 % ${#keys[@]} -ne 0, portanto, o erro se agrava.

Isso deve ser imparcial e funciona com qualquer nome de arquivo:

while IFS= read -r -d '' -u 9
do
    mv -- "$REPLY" /target/dir
done 9< <(find /source/dir -mindepth 1 -print0 | shuf -n 200 -z)

2

A solução de Kevin funciona muito bem! Outra coisa que eu usei muito, porque acho mais fácil lembrar de cima, é algo como:

cp `ls | shuf -n 200` destination

0

Um forro no bash:

ls original_directory/|sort -R|head -number_of_files_to_move|while read file; do cp "new_directory/"$file test; done

Por favor elabore; U&L é uma base de conhecimento.
— countermode
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.