UDFs Python vectorisées

Cette rubrique présente les UDFs Python vectorisées.

Dans ce chapitre :

Vue d’ensemble

Les UDFs Python vectorisées vous permettent de définir des fonctions Python qui reçoivent des lots de lignes d’entrée sous forme de DataFrames Pandas et renvoient des lots de résultats sous forme de tableaux ou de séries Pandas. Vous appelez des UDFs Python vectorisées de la même manière que vous appelez les autres UDFs Python.

Les avantages de l’utilisation des UDFs Python vectorisées par rapport au modèle de traitement ligne par ligne par défaut sont les suivants :

  • Le potentiel de meilleures performances si votre code Python fonctionne efficacement sur des lots de lignes.

  • Moins de logique de transformation requise si vous faites appel à des bibliothèques qui fonctionnent sur des DataFrames Pandas ou des tableaux Pandas.

Lorsque vous utilisez des UDFs Python vectorisées :

  • Vous n’avez pas besoin de modifier la façon dont vous écrivez les requêtes en utilisant des UDFs Python. Tous les lots sont gérés par le framework UDF plutôt que par votre propre code.

  • Comme pour les UDFs qui ne sont pas vectorisées, il n’y a aucune garantie quant aux instances de votre code de gestionnaire qui verront les lots d’entrée.

Premiers pas avec les UDFs Python vectorisées

Pour créer une UDF Python vectorisée, utilisez l’un des mécanismes pris en charge pour annoter votre fonction de gestionnaire.

Utilisation du décorateur vectorized

Le module _snowflake est exposé à des UDFs Python qui s’exécutent dans Snowflake. Dans votre code Python, importez le module _snowflake et utilisez le décorateur vectorized pour spécifier que votre gestionnaire s’attend à recevoir un DataFrame Pandas en définissant le paramètre pandas.DataFrame sur input.

create function add_one_to_inputs(x number(10, 0), y number(10, 0))
returns number(10, 0)
language python
runtime_version = 3.8
packages = ('pandas')
handler = 'add_one_to_inputs'
as $$
import pandas
from _snowflake import vectorized

@vectorized(input=pandas.DataFrame)
def add_one_to_inputs(df):
  return df[0] + df[1] + 1
$$;
Copy

Utilisation d’un attribut de fonction

Plutôt que d’importer le module _snowflake et d’utiliser le décorateur vectorized, vous pouvez définir l’attribut spécial _sf_vectorized_input sur votre fonction de gestionnaire.

create function add_one_to_inputs(x number(10, 0), y number(10, 0))
returns number(10, 0)
language python
runtime_version = 3.8
packages = ('pandas')
handler = 'add_one_to_inputs'
as $$
import pandas

def add_one_to_inputs(df):
  return df[0] + df[1] + 1

add_one_to_inputs._sf_vectorized_input = pandas.DataFrame
$$;
Copy

Définition d’une taille de lot cible

Les appels à la fonction de gestionnaire Python doivent s’exécuter dans un délai limité, qui est de 180 secondes, et chaque DataFrame transmis en entrée à la fonction de gestionnaire peut actuellement contenir jusqu’à quelques milliers de lignes. Afin de respecter la limite de temps, vous pouvez définir la taille de lot cible pour votre fonction de gestionnaire, qui impose un nombre maximal de lignes par DataFrame d’entrée. Notez que la définition d’une valeur plus grande ne garantit pas que Snowflake codera les lots avec le nombre de lignes spécifié. Vous pouvez définir la taille du lot cible en utilisant soit le décorateur vectorized soit un attribut de la fonction.

Note

L’utilisation de max_batch_size est uniquement destinée à limiter le nombre de lignes que l’UDF peut traiter par lot. Par exemple, si l’UDF est écrite de manière à ne pouvoir traiter que 100 lignes au maximum à la fois, alors max_batch_size doit être défini comme 100. Le paramètre max_batch_size n’est pas destiné à être utilisé comme un mécanisme permettant de spécifier des tailles de lot arbitraires et importantes. Si l’UDF est capable de traiter des lots de n’importe quelle taille, il est recommandé de ne pas définir ce paramètre.

Utilisation du décorateur vectorized

Pour définir la taille de lot cible à l’aide du décorateur vectorized passez une valeur entière positive pour l’argument nommé max_batch_size.

Par exemple, cette instruction crée une UDF Python vectorisée et limite chaque dataframe à un maximum de 100 lignes :

create function add_one_to_inputs(x number(10, 0), y number(10, 0))
returns number(10, 0)
language python
runtime_version = 3.8
packages = ('pandas')
handler = 'add_one_to_inputs'
as $$
import pandas
from _snowflake import vectorized

@vectorized(input=pandas.DataFrame, max_batch_size=100)
def add_one_to_inputs(df):
  return df[0] + df[1] + 1
$$;
Copy

Utilisation d’un attribut de fonction

Pour définir la taille de lot cible à l’aide d’un attribut de fonction, définissez une valeur entière positive pour l’attribut _sf_max_batch_size de votre fonction de gestionnaire.

Par exemple, cette instruction crée une UDF Python vectorisée et limite chaque DataFrame à un maximum de 100 lignes :

create function add_one_to_inputs(x number(10, 0), y number(10, 0))
returns number(10, 0)
language python
runtime_version = 3.8
packages = ('pandas')
handler = 'add_one_to_inputs'
as $$
import pandas

def add_one_to_inputs(df):
  return df[0] + df[1] + 1

add_one_to_inputs._sf_vectorized_input = pandas.DataFrame
add_one_to_inputs._sf_max_batch_size = 100
$$;
Copy

Encodage DataFrame

Les lots d’arguments de l’UDF sont codés sous forme de tableaux dans les DataFrames Pandas d’entrée, et le nombre de lignes dans chaque DataFrame peut varier. Pour plus d’informations, voir Définir une taille de lot cible. Les arguments sont accessibles dans le DataFrame par leur index, c’est-à-dire que le premier argument a un index de 0, le second a un index de 1, et ainsi de suite. Le tableau ou la série Pandas que le gestionnaire UDF renvoie doit avoir la même longueur que le DataFrame d’entrée.

Pour illustrer ce propos, supposons que vous définissiez une UDF Python vectorisée comme suit :

create or replace function add_inputs(x int, y float)
returns float
language python
runtime_version = 3.8
packages = ('pandas')
handler = 'add_inputs'
as $$
import pandas
from _snowflake import vectorized

@vectorized(input=pandas.DataFrame)
def add_inputs(df):
  return df[0] + df[1]
$$;
Copy

Cette UDF utilise df[0] pour accéder au tableau Pandas pour le premier argument et df[1] pour le second. df[0] + df[1] donne un tableau Pandas contenant les sommes par paires des éléments correspondants des deux tableaux. Après avoir créé l’UDF, vous pouvez l’appeler avec quelques lignes d’entrée :

select add_inputs(x, y)
from (
  select 1 as x, 3.14::float as y union all
  select 2, 1.59 union all
  select 3, -0.5
);
+------------------+
| ADD_INPUTS(X, Y) |
|------------------|
|             4.14 |
|             3.59 |
|             2.5  |
+------------------+
Copy

Ici, la fonction Python add_inputs reçoit un DataFrame analogue à celui créé avec le code Python suivant :

>>> import pandas
>>> df = pandas.DataFrame({0: pandas.array([1, 2, 3]), 1: pandas.array([3.14, 1.59, -0.5])})
>>> df
   0     1
0  1  3.14
1  2  1.59
2  3 -0.50
Copy

La ligne return df[0] + df[1] de la fonction du gestionnaire donne un tableau similaire au code Python suivant :

>>> df[0] + df[1]
0    4.14
1    3.59
2    2.50
dtype: float64
Copy

Prise en charge du type

Les UDFs Python vectorisées prennent en charge les types SQL suivants pour les arguments et les valeurs de retour. Le tableau reflète la manière dont chaque argument SQL est codé en tant que tableau Pandas d’un dtype particulier.

Type SQL

Dtype Pandas

Remarques

NUMBER

Int16, Int32, ou Int64 pour NUMBER arguments avec une échelle de 0 qui tiennent tous dans un type d’entier de 64 bits ou plus petit. Si l’argument ne peut pas devenir null, int16, int32 ou int64 est utilisé à la place. (pour des UDTFs, Int16, Int32, ou Int64 sera utilisé à la place.) . . object pour les arguments dont l’échelle est différente de 0, ou pour les arguments qui ne tiennent pas dans un entier de 64 bits, où les éléments du tableau sont codés comme des valeurs decimal.Decimal. . . Pour garantir un dtype de 16 bits, utilisez une précision NUMBER maximale de 4. Pour garantir un dtype de 32 bits, utilisez une précision NUMBER maximale de 9. Pour garantir un dtype de 64 bits, utilisez une précision NUMBER maximale de 18.

Pour s’assurer qu’un argument d’entrée d’une UDF est interprété comme ne pouvant pas être null, validez une colonne d’une table créée à l’aide de la contrainte de colonne NOT NULL, ou utilisez une fonction telle que IFNULL sur l’argument.

FLOAT

float64

Les valeurs NULL sont codées comme des valeurs NaN. Dans la sortie, les valeurs NaN sont interprétées comme NULLs.

BOOLEAN

boolean pour les arguments pouvant être null ou bool pour les arguments ne pouvant pas être null.

VARCHAR

string

Snowflake SQL et Pandas représentent tous deux les chaînes qui utilisent l’encodage UTF-8.

BINARY

bytes

DATE

datetime64

Chaque valeur est codée comme un datetime64 sans composante temporelle. Les valeurs NULL sont encodées en tant que numpy.timedelta('NaT').

VARIANT

object . . Chaque valeur est encodée comme un dict, list, int, float, str ou bool.

Chaque ligne de variante est convertie dynamiquement en un type Python pour les arguments et vice versa pour les valeurs de retour. Les types suivants sont convertis en chaînes plutôt qu’en types Python natifs : decimal, binary, date, time, timestamp_ltz, timestamp_ntz, timestamp_tz.

OBJECT

object . . Chaque élément est encodé comme un dict.

ARRAY

object . . Chaque élément est encodé comme une liste.

TIME

timedelta64

Chaque valeur est encodée comme un décalage par rapport à minuit. Les valeurs NULL sont encodées en tant que numpy.timedelta64('NaT'). Lorsqu’ils sont utilisés comme types de retour, les éléments de la sortie peuvent être des valeurs numpy.timedelta64 ou datetime.time dans la plage [00:00:00, 23:59:59.999999999].

TIMESTAMP_LTZ

datetime64

Utilise le fuseau horaire local pour encoder chaque valeur sous forme de nanoseconde numpy.datetime64 par rapport à l’époque Unix UTC. Les valeurs NULL sont encodées en tant que numpy.datetime64('NaT'). Lorsqu’ils sont utilisés comme types de retour, les éléments de la sortie peuvent être des valeurs numpy.datetime64 ou datetime.datetime naïves de fuseau horaire ou pandas.Timestamp

TIMESTAMP_NTZ

datetime64

Encode chaque valeur comme un numpy.datetime64 à l’échelle de la nanoseconde. Les valeurs NULL sont encodées en tant que numpy.datetime64('NaT'). Lorsqu’ils sont utilisés comme types de retour, les éléments de la sortie peuvent être des valeurs numpy.datetime64 ou datetime.datetime naïves de fuseau horaire ou pandas.Timestamp

TIMESTAMP_TZ

object

Encode chaque valeur comme un pandas.Timestamp à l’échelle de la nanoseconde. Les valeurs NULL sont encodées en tant que pandas.NA. Lorsqu’ils sont utilisés comme types de retour, les éléments de la sortie peuvent être des valeurs datetime.datetime sensibles au fuseau horaire ou pandas.Timestamp.

GEOGRAPHY

object

Formate chaque valeur en GeoJSON et la convertit ensuite en un dict Python.

Les types suivants sont acceptés en sortie : Pandas Series ou array, NumPy array, Python ordinaire list, et toute séquence itérable contenant les types attendus décrits dans Prise en charge du type. Il est efficace d’utiliser Pandas Series et array et NumPy array lorsque le dtype est bool, boolean, int16, int32, int64, Int16, Int32, Int64 ou float64 car ces formats exposent leur contenu comme memoryviews. Cela signifie que le contenu peut être copié plutôt que chaque valeur soit lue de manière séquentielle.