Adeus Pandas! Analisando dados com cuDF (NVIDIA RAPIDS) #2

Rafael Faria de Azevedo
9 min readJan 14, 2023

--

A ideia deste post é dar continuidade ao assunto NVIDIA RAPIDS que comecei tratando na publicação anterior (Como usar Random Forest, SVM e outros algoritmos de Shallow Machine Learning na GPU?). O primeiro post tratou de um dos componentes do RAPIDS o cuML (uma alternativa ao scikit-learn que roda na GPU). Neste post, será apresentado um outro componente do RAPIDS, o cuDF.

E por que você deve ler esta matéria? Primeiramente, se você trabalha com ciência de dados (machine learning ou deep learning), ou se trabalha com dados em outra área e utiliza a biblioteca Pandas. Simplesmente porque utilizar o Pandas ou o cuDF pode fazer uma grande diferença no seu tempo de processamento. Nos testes feitos para esta postagem, o cuDF chegou a ser mais de 100 vezes mais rápido que o Pandas. Todavia, para ter essa opção, você precisa ter disponível uma GPU NVIDIA com alguns pré-requisitos que serão apresentados ao longo da matéria. Seja bem-vindo(a)!

Um Dataframe de canecas e xícaras?🤔😍😋🤭

Tópicos da publicação

Funções gerais
Dataset
Experimentos
1. Dataframe
2. head (função)
3. describe (função)
4. sort_values (função) 1
5. query (função)
6. apply (função)
7. value_counts (função)
8. groupby (função)
9. sort_values (função) 2
10. Tabela com o resumo dos experimentos
11. Notebook e log (GitHub)
12. Como instalar o RAPIDS?
13. Referências
14. Contato

Sem enrolação, vamos lá!

Começo apresentando a máquina utilizada, a mesma do post passado:

Driver version: 470.161.03
Modelo da GPU: 1 x NVIDIA GeForce RTX 2060 (laptop)
Memória total da GPU: 6 GB
CUDA version: 11.4
Python version: 3.9.15
Sistema operacional: Ubuntu 18.04.6 LTS (Bionic Beaver)
Memória RAM total disponível: 16 GB
Padrão da memória RAM: DDR4
Tipo de storage: NVMe
Modelo do processador: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Total de núcleos virtuais do processador: 12
IDE utilizada: VSCode + Anaconda

Os pacotes importados e suas versões foram:

import time
import datetime
import pandas as pd
import cupy as cp
import cudf
import cupy
import psutil
from loguru import logger
import platform

logger.add("medium2-cudf.log")
logger.info(f"cudf.__version__: {cudf.__version__}")
logger.info(f"cupy.__version__: {cupy.__version__}")
logger.info(f"pandas.__version__: {pd.__version__}")

from pynvml import *
nvmlInit()
OUTPUT:
cudf.__version__: 22.12.01
cupy.__version__: 11.4.0
pandas.__version__: 1.5.2

Funções gerais

Criei algumas funções para tornar o código menor e mais objetivo. As duas primeiras foram utilizadas para marcar o tempo inicial e final do processamento de cada experimento. A terceira função foi utilizada para apresentar o resultado (And the winner is ????) da comparação entre Pandas e cuDF.

def time_init(experimento):
logger.info(f"Nome do experimento: {experimento}")
time_start = time.time()
datetime_start = datetime.datetime.now()
return datetime_start, time_start
def time_print(datetime_start, time_start):
time_end = time.time()
datetime_end = datetime.datetime.now()
elapsed_datetime = (datetime_end - datetime_start)
elapsed_time = (time_end - time_start)
logger.warning(f"Tempo de execução (datetime): {str(elapsed_datetime)} segundos")
logger.warning(f"Tempo de execução (time): {str(elapsed_time)} segundos")
return elapsed_time
def print_performance(rapids, pandas):
exp1 = 'PANDAS' if pandas < rapids else 'CUDF'
exp2 = 'PANDAS' if pandas > rapids else 'CUDF'
exp3 = (pandas/rapids if pandas > rapids else rapids/pandas)
exp4 = exp3 >= 2
exp5 = ('vezes' if exp4 else 'vez')
logger.success(f"RESULTADO: {exp1} foi mais rápido que o {exp2} {exp3} {exp5}")

Dataset

Para este publicação, foi criado um dataset aleatório. Ele possui 6 milhões de registros e 10 variáveis/características (features), mais a classe alvo (5 classes no total).

Experimentos

Cada experimento é apresentado na sequência.

1. Dataframe

O primeiro experimento compara a performance no carregamento de um arquivo CSV através de um Dataframe Pandas e também cuDF. O arquivo utilizado (medium2_dt1_6m.csv) é o descrito na seção Dataset acima.

As funções abaixo foram utilizadas neste experimento.

def load_dataset_cudf(filename):
'''
Carregando os dados usando o CUDF.
'''
logger.info(f"filename: {filename}")
cu_df = cudf.read_csv(filename)
logger.info(f"cu_df.shape: {cu_df.shape}")
return cu_df
def load_dataset_pandas(filename):
'''
Carregando os dados usando o Pandas.
'''
logger.info(f"filename: {filename}")
df = pd.read_csv(filename)
logger.info(f"df.shape: {df.shape}")
return df
filename = "medium2_dt1_6m.csv"

Segue o experimento com o cuDF e sua saída.

# cudf
datetime_start, time_start = time_init('1. Dataframe - cudf')
# Carregando o Dataframe CUDF
dfc = load_dataset_cudf(filename)
rap = time_print(datetime_start, time_start)
dfc
OUTPUT:
Nome do experimento: 1. Dataframe - cudf
filename: medium2_dt1_6m.csv
cu_df.shape: (6000000, 11)
Tempo de execução (datetime): 0:00:04.275779 segundos
Tempo de execução (time): 4.275778532028198 segundos

Agora o experimento com o Pandas e sua saída.

# Pandas
datetime_start, time_start = time_init('1. Dataframe - Pandas')
# Carregando o Dataframe PANDAS
dfp = load_dataset_pandas(filename)
pan = time_print(datetime_start, time_start)
print_performance(rap, pan)
dfp
OUTPUT:
Nome do experimento: 1. Dataframe - Pandas
filename: medium2_dt1_6m.csv
df.shape: (6000000, 11)
Tempo de execução (datetime): 0:00:05.074893 segundos
Tempo de execução (time): 5.074894189834595 segundos

Abaixo temos a saída (output) de ambas execuções.

RESULTADO: CUDF foi mais rápido que o PANDAS 1.1868936035439 vez

2. head

Comparação da função head.

# Pandas
datetime_start, time_start = time_init('2. head - Pandas')
out1 = dfp.head()
pan = time_print(datetime_start, time_start)
out1
OUTPUT:
Nome do experimento: 2. head - Pandas
Tempo de execução (datetime): 0:00:00.000300 segundos
Tempo de execução (time): 0.0002999305725097656 segundos
# cudf
datetime_start, time_start = time_init('2. head - cudf')
out2 = dfc.head()
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
out2
OUTPUT:
Nome do experimento: 2. head - cudf
Tempo de execução (datetime): 0:00:00.001017 segundos
Tempo de execução (time): 0.0010161399841308594 segundos

Saída (output) do experimento para o cuDF e para o Pandas.

RESULTADO: PANDAS foi mais rápido que o CUDF 3.3879173290937996 vezes

3. describe

Teste com a função describe.

# Pandas
datetime_start, time_start = time_init('3. describe - Pandas')
dfp.describe()
pan = time_print(datetime_start, time_start)
OUTPUT:
Nome do experimento: 3. describe - Pandas
Tempo de execução (datetime): 0:00:01.877654 segundos
Tempo de execução (time): 1.8776533603668213 segundos
# cudf
datetime_start, time_start = time_init('3. describe - cudf')
dfc.describe()
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
OUTPUT:
Nome do experimento: 3. describe - cudf
Tempo de execução (datetime): 0:00:00.960006 segundos
Tempo de execução (time): 0.9600050449371338 segundos
RESULTADO: CUDF foi mais rápido que o PANDAS 1.9558786386271334 vez

4. sort_values 1

Experimento com a função sort_values.

arg1 = 'ft_5'
# Pandas
datetime_start, time_start = time_init('4. sort_values 1 - Pandas')
out3 = dfp.sort_values(by=arg1)
pan = time_print(datetime_start, time_start)
out3
OUTPUT:
Nome do experimento: 4. sort_values 1 - Pandas
Tempo de execução (datetime): 0:00:01.426562 segundos
Tempo de execução (time): 1.4265611171722412 segundos
# cudf
datetime_start, time_start = time_init('4. sort_values 1 - cudf')
out4 = dfc.sort_values(by=arg1)
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
out4
OUTPUT:
Nome do experimento: 4. sort_values 1 - cudf
Tempo de execução (datetime): 0:00:00.126864 segundos
Tempo de execução (time): 0.12686395645141602 segundos
RESULTADO: CUDF foi mais rápido que o PANDAS 11.244810244575328 vezes

5. query

Avaliando a função query.

arg2 = "ft_3 > 0.7"
# Pandas
datetime_start, time_start = time_init('5. query - Pandas')
out5 = dfp.query(arg2)
pan = time_print(datetime_start, time_start)
out5
OUTPUT:
Nome do experimento: 5. query - Pandas
Tempo de execução (datetime): 0:00:00.118219 segundos
Tempo de execução (time): 0.11821746826171875 segundos
# cudf
datetime_start, time_start = time_init('5. query - cudf')
out6 = dfc.query(arg2)
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
out6
OUTPUT:
Nome do experimento: 5. query - cudf
Tempo de execução (datetime): 0:00:00.241971 segundos
Tempo de execução (time): 0.24196934700012207 segundos
RESULTADO: PANDAS foi mais rápido que o CUDF 2.0468155050016135 vezes

6. apply

Teste com a função apply.

def add_ten(num):
return num + 10
# Pandas
datetime_start, time_start = time_init('6. apply - Pandas')
out7 = dfp['ft_7'].apply(add_ten)
pan = time_print(datetime_start, time_start)
out7
OUTPUT:
Nome do experimento: 6. apply - Pandas
Tempo de execução (datetime): 0:00:01.057686 segundos
Tempo de execução (time): 1.0576858520507812 segundos
# cudf
datetime_start, time_start = time_init('6. apply - cudf')
out8 = dfc['ft_7'].apply(add_ten)
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
out8
OUTPUT:
Nome do experimento: 6. apply - cudf
Tempo de execução (datetime): 0:00:00.077309 segundos
Tempo de execução (time): 0.07730793952941895 segundos

Saída (output) antes da aplicação da função em ambas as bibliotecas.

Saída (output) depois da aplicação da função em ambas as bibliotecas.

RESULTADO: CUDF foi mais rápido que o PANDAS 13.681464782130003 vezes

7. value_counts

Experimento com a função value_counts.

# Pandas
datetime_start, time_start = time_init('7. value_counts - Pandas')
dfp.ft_9.value_counts()
pan = time_print(datetime_start, time_start)
OUTPUT:
Nome do experimento: 7. value_counts - Pandas
Tempo de execução (datetime): 0:00:00.978318 segundos
Tempo de execução (time): 0.9783174991607666 segundos
# cudf
datetime_start, time_start = time_init('7. value_counts - cudf')
dfc.ft_9.value_counts()
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
OUTPUT:
Nome do experimento: 7. value_counts - cudf
Tempo de execução (datetime): 0:00:00.043801 segundos
Tempo de execução (time): 0.04380059242248535 segundos
RESULTADO: CUDF foi mais rápido que o PANDAS 22.335713858028555 vezes

8. groupby

Avaliação da função groupby.

# Pandas
datetime_start, time_start = time_init('8. groupby - Pandas')
dfp.groupby(['ft_2', 'ft_7']).size().sort_values(ascending=True)
pan = time_print(datetime_start, time_start)
OUTPUT:
Nome do experimento: 8. groupby - Pandas
Tempo de execução (datetime): 0:00:06.394761 segundos
Tempo de execução (time): 6.394759178161621 segundos
# cudf
datetime_start, time_start = time_init('8. groupby - cudf')
dfc.groupby(['ft_2', 'ft_7']).size().sort_values(ascending=True)
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
OUTPUT:
Nome do experimento: 8. groupby - cudf
Tempo de execução (datetime): 0:00:00.056922 segundos
Tempo de execução (time): 0.056920528411865234 segundos
RESULTADO: CUDF foi mais rápido que o PANDAS 112.34539377235677 vezes

9. sort_values 2

Outro teste com a função sort_values.

arg3 = ['class', 'ft_1', 'ft_2', 'ft_3', 'ft_4', 'ft_5', 'ft_6', 'ft_7', 'ft_9']
# Pandas
datetime_start, time_start = time_init('9. sort_values 2 - Pandas')
out9 = dfp.sort_values(by=arg3)
pan = time_print(datetime_start, time_start)
out9
OUTPUT:
Nome do experimento: 9. sort_values 2 - Pandas
Tempo de execução (datetime): 0:00:34.541925 segundos
Tempo de execução (time): 34.541924238204956 segundos
# cudf
datetime_start, time_start = time_init('9. sort_values 2 - cudf')
out10 = dfc.sort_values(by=arg3)
rap = time_print(datetime_start, time_start)
print_performance(rap, pan)
out10
OUTPUT:
Nome do experimento: 9. sort_values 2 - cudf
Tempo de execução (datetime): 0:00:00.296054 segundos
Tempo de execução (time): 0.2960529327392578 segundos

A mesma saída (output) do Pandas e do cuDF.

RESULTADO: CUDF foi mais rápido que o PANDAS 116.67482540572232 vezes

10. Tabela com o resumo dos experimentos

11. Observações

A utilização do Pandas ou do cuDF obviamente vai depender de ter uma GPU com os pré-requisitos necessários primeiramente. Porém, outras questões devem ser observadas, como o tamanho do dataset caber na memória da GPU ou não. Assim como a própria tarefa a ser feita, visto que em alguns casos o Pandas foi mais rápido que o cuDF. Como quase sempre no mundo da inteligência artificial e em nossas vidas (inteligência humana), não existe uma bala de prata, cada caso tende a ser único.

11. Notebook e log (GitHub)

O notebook criado para este post e o arquivo de log podem ser acessados no meu GitHub (aqui).

Não pude colocar o dataset utilizado, pois o arquivo é maior que o Git aceita. O arquivo medium2_dt1_6m.csv tem 756,2 MB.

Porém, isso não prejudica o post, pois o objetivo dele é que você teste o cuDF com o dataset que quiser, ou seu hardware aguentar 😁.

Uma última coisa. Sim, o título deste post é exagerado, 😂!

12. Como instalar o RAPIDS?

1. Execute o comando “nvidia-smi”, para saber qual o CUDA Version (pode ser CUDA 11.2, CUDA 11.4 e CUDA 11.5) da sua máquina com placa de vídeo dedicada da NVIDIA;

2. Execute o comando “python --version”, para saber qual a versão do Python instalada na máquina (pode ser Python 3.8 ou 3.9);

3. Preencha os parâmetros em https://rapids.ai/start.html (STEP 3: INSTALL RAPIDS), no final você terá o comando para montar um ambiente Conda com o Rapids;

4. Divirta-se!

13. Referências

14. Contato

https://www.linkedin.com/in/rafael-faria-de-azevedo-msc-a9855246/

--

--

Rafael Faria de Azevedo

Estudo machine learning desde 2016. Trabalho com o tema desde 2018. Gosto de correr, tocar baixo, estudar inteligência artificial e filosofia.