web-dev-qa-db-fra.com

Comment lire une liste de fichiers de parquet de S3 en tant que frame de données pandas avec pyarrow

J’ai un moyen simple d’y parvenir en utilisant boto3 (1.4.4), pyarrow (0.4.1) et pandas (0.20.3).

Premièrement, je peux lire un seul fichier de parquet localement comme ceci:

import pyarrow.parquet as pq

path = 'parquet/part-r-00000-1e638be4-e31f-498a-a359-47d017a0059c.gz.parquet'
table = pq.read_table(path)
df = table.to_pandas()

Je peux aussi lire un répertoire de fichiers de parquet localement comme ceci:

import pyarrow.parquet as pq

dataset = pq.ParquetDataset('parquet/')
table = dataset.read()
df = table.to_pandas()

Les deux fonctionnent comme un charme. Maintenant, je veux atteindre le même objectif à distance avec les fichiers stockés dans un compartiment S3. J'espérais que quelque chose comme ça marcherait:

dataset = pq.ParquetDataset('s3n://dsn/to/my/bucket')

Mais ce n'est pas le cas:

OSError: Passed non-file path: s3n://dsn/to/my/bucket

Après avoir lu la documentation de pyarrow à fond, cela ne semble pas possible pour le moment . Alors je suis sorti avec la solution suivante:

Lire un seul fichier depuis S3 et obtenir un cadre de données pandas:

import io
import boto3
import pyarrow.parquet as pq

buffer = io.BytesIO()
s3 = boto3.resource('s3')
s3_object = s3.Object('bucket-name', 'key/to/parquet/file.gz.parquet')
s3_object.download_fileobj(buffer)
table = pq.read_table(buffer)
df = table.to_pandas()

Et voici mon hacky, solution non optimisée, pour créer un cadre de données pandas à partir d’un chemin de dossier S3:

import io
import boto3
import pandas as pd
import pyarrow.parquet as pq

bucket_name = 'bucket-name'
def download_s3_parquet_file(s3, bucket, key):
    buffer = io.BytesIO()
    s3.Object(bucket, key).download_fileobj(buffer)
    return buffer

client = boto3.client('s3')
s3 = boto3.resource('s3')
objects_dict = client.list_objects_v2(Bucket=bucket_name, Prefix='my/folder/prefix')
s3_keys = [item['Key'] for item in objects_dict['Contents'] if item['Key'].endswith('.parquet')]
buffers = [download_s3_parquet_file(s3, bucket_name, key) for key in s3_keys]
dfs = [pq.read_table(buffer).to_pandas() for buffer in buffers]
df = pd.concat(dfs, ignore_index=True)

Y a-t-il un meilleur moyen d'y parvenir? Peut-être une sorte de connecteur pour les pandas utilisant pyarrow? J'aimerais éviter d'utiliser pyspark, mais s'il n'y a pas d'autre solution, je la prendrais.

16
Diego Mora Cespedes

Vous devriez utiliser le module s3fs proposé par yjk21 . Cependant, si vous appelez ParquetDataset, vous obtiendrez un objet pyarrow.parquet.ParquetDataset. Pour obtenir le Pandas DataFrame, vous voudrez plutôt lui appliquer .read_pandas().to_pandas():

import pyarrow.parquet as pq
import s3fs
s3 = s3fs.S3FileSystem()

pandas_dataframe = pq.ParquetDataset('s3://your-bucket/', filesystem=s3).read_pandas().to_pandas()
16
vak

Vous pouvez utiliser s3fs from dask qui implémente une interface de système de fichiers pour s3. Ensuite, vous pouvez utiliser l’argument du système de fichiers de ParquetDataset comme ceci:

import s3fs
s3 = s3fs.S3FileSystem()
dataset = pq.ParquetDataset('s3n://dsn/to/my/bucket', filesystem=s3)
4
yjk21

Boto3 peut aussi être utilisé sans pyarrow

import boto3
import io
import pandas as pd

# Read the parquet file
buffer = io.BytesIO()
s3 = boto3.resource('s3')
object = s3.Object('bucket_name','key')
object.download_fileobj(buffer)
df = pd.read_parquet(buffer)

print(df.head())
3
oya163

Le moyen le plus simple de lire les données de parquet sur le nuage en images est d’utiliser dask.dataframe de cette façon:

import dask.dataframe as dd
df = dd.read_parquet('s3://bucket/path/to/data-*.parq')

dask.dataframe peut lire sur Google Cloud Storage, Amazon S3, le système de fichiers Hadoop et plus encore

2
Rich Signell

Merci! Votre question en dit beaucoup. Voici comment je le fais maintenant avec pandas (0.21.1), qui appellera pyarrow et boto3 (1.3.1).

import boto3
import io
import pandas as pd

# Read single parquet file from S3
def pd_read_s3_parquet(key, bucket, s3_client=None, **args):
    if s3_client is None:
        s3_client = boto3.client('s3')
    obj = s3_client.get_object(Bucket=bucket, Key=key)
    return pd.read_parquet(io.BytesIO(obj['Body'].read()), **args)

# Read multiple parquets from a folder on S3 generated by spark
def pd_read_s3_multiple_parquets(filepath, bucket, s3=None, 
                                 s3_client=None, verbose=False, **args):
    if not filepath.endswith('/'):
        filepath = filepath + '/'  # Add '/' to the end
    if s3_client is None:
        s3_client = boto3.client('s3')
    if s3 is None:
        s3 = boto3.resource('s3')
    s3_keys = [item.key for item in s3.Bucket(bucket).objects.filter(Prefix=filepath)
               if item.key.endswith('.parquet')]
    if not s3_keys:
        print('No parquet found in', bucket, filepath)
    Elif verbose:
        print('Load parquets:')
        for p in s3_keys: 
            print(p)
    dfs = [pd_read_s3_parquet(key, bucket=bucket, s3_client=s3_client, **args) 
           for key in s3_keys]
    return pd.concat(dfs, ignore_index=True)

Ensuite, vous pouvez lire plusieurs parquets sous un dossier de S3 en

df = pd_read_s3_multiple_parquets('path/to/folder', 'my_bucket')

(On peut simplifier beaucoup ce code, je suppose.)

1
Louis Yang