PyODPS DataFrame では、ユーザー定義関数 (UDF) とサードパーティ Python ライブラリを使用して組み込み演算を拡張できます。このトピックでは、map による要素単位のマッピング、apply による行レベルの変換とカスタム集計、UDF 内のリソース参照、およびサードパーティ Python ライブラリのアップロードと設定方法について説明します。
前提条件
開始する前に、以下を確認してください。
-
MaxCompute テーブルまたは pandas DataFrame から作成された DataFrame オブジェクトがあること
-
MaxCompute プロジェクトで Python UDF サポートが有効になっていること (
mapとapplyに Python 関数を使用するために必要)
Alibaba Cloud パブリックサービスは Python UDF をサポートしていません。プロジェクトが Python UDF をサポートしていない場合、map メソッドおよびそれに依存する組み込み関数は使用できません。
既知の制限
| 制限 | 詳細 |
|---|---|
| サポートされていない型 | map および apply メソッドは、入力または出力として LIST 型や DICT 型を受け付けません。 |
| プリインストールされたバイナリライブラリ | C コードを含むプリインストールされたサードパーティライブラリは NumPy のみです。その他のバイナリライブラリは明示的にアップロードする必要があります。 |
| Python 2/3 互換性 | Python バージョン間のバイトコードの違いにより、Python 3 固有の構文 (yield from など) を使用したコードは、Python 2.7 を実行している MaxCompute Worker で失敗する可能性があります。本番環境にデプロイする前に、Python 3 の MapReduce API を使用してコードが正しく動作することを確認してください。 |
| バイナリパッケージのビルドプラットフォーム | macOS または Windows でビルドされた Wheel ファイルは MaxCompute では使用できません。バイナリパッケージは Linux シェルでビルドしてください。 |
列への UDF の適用
Sequence オブジェクトの map メソッドを使用して、すべての要素に UDF を適用します。
>>> iris.sepallength.map(lambda x: x + 1).head(5)
sepallength
0 6.1
1 5.9
2 5.7
3 5.6
4 6.0
map の後に Sequence の型が変わる場合は、新しい型を明示的に指定してください。
>>> iris.sepallength.map(lambda x: 't' + str(x), 'string').head(5)
sepallength
0 t5.1
1 t4.9
2 t4.7
3 t4.6
4 t5.0
クロージャ変数のキャプチャバグの回避
UDF にクロージャが含まれている場合、キャプチャされた変数への外部からの変更が関数の動作に影響します。以下のコードは意図しない結果になります。 dfs 内の各 SequenceExpr は最終的に df.sepal_length + 9 になります。
>>> dfs = []
>>> for i in range(10):
>>> dfs.append(df.sepal_length.map(lambda x: x + i))
これを修正するには、外側の関数からラムダを返すか、functools.partial を使用してください。
# オプション 1:ファクトリ関数を使用
>>> dfs = []
>>> def get_mapper(i):
>>> return lambda x: x + i
>>> for i in range(10):
>>> dfs.append(df.sepal_length.map(get_mapper(i)))# オプション 2:functools.partial を使用
>>> import functools
>>> dfs = []
>>> for i in range(10):
>>> dfs.append(df.sepal_length.map(functools.partial(lambda v, x: x + v, i)))
既存の UDF の使用
関数名 (文字列) または Function オブジェクトを map に渡すことで、既存の UDF を呼び出せます。詳細については、「関数」をご参照ください。
カウンタによる実行の追跡
get_execution_context を使用して、UDF 内からカウンタにアクセスします。カウンタの値は LogView の JSONSummary に表示されます。
from odps.udf import get_execution_context
def h(x):
ctx = get_execution_context()
counters = ctx.get_counters()
counters.get_counter('df', 'add_one').increment(1)
return x + 1
df.field.map(h)
行への UDF の適用
apply と axis=1 を使用して、各行に UDF を適用します。UDF は一度に 1 行を受け取ります。フィールド値は属性名またはインデックスで取得できます。
行ごとに単一の値を返す
reduce=True を設定すると、Sequence が返ります。出力型は types パラメータで指定します (デフォルトは STRING)。
>>> iris.apply(lambda row: row.sepallength + row.sepalwidth, axis=1, reduce=True, types='float').rename('sepaladd').head(3)
sepaladd
0 8.6
1 7.9
2 7.9
yield を使用した複数行の返却
reduce=False を設定し、yield を使用して入力行ごとに複数の行を出力します。出力フィールド名と型は names と types で指定します。
>>> iris.count()
150
>>> def handle(row):
>>> yield row.sepallength - row.sepalwidth, row.sepallength + row.sepalwidth
>>> yield row.petallength - row.petalwidth, row.petallength + row.petalwidth
>>> iris.apply(handle, axis=1, names=['iris_sub', 'iris_add'], types=['float', 'float']).count()
300
呼び出し側で繰り返し指定するのを避けるため、関数に直接出力スキーマを注釈として付与できます。
>>> from odps.df import output
>>> @output(['iris_sub', 'iris_add'], ['float', 'float'])
>>> def handle(row):
>>> yield row.sepallength - row.sepalwidth, row.sepallength + row.sepalwidth
>>> yield row.petallength - row.petalwidth, row.petallength + row.petalwidth
>>> iris.apply(handle, axis=1).count()
300
同等の処理:map のみの map_reduce
map のみのモードの map_reduce は、apply と axis=1 と同等です。
>>> iris.map_reduce(mapper=handle).count()
300
既存の UDTF の使用
MaxCompute の既存のユーザー定義テーブル値関数 (UDTF) を呼び出すには、その名前を文字列として渡します。
>>> iris['name', 'sepallength'].apply('your_func', axis=1, names=['name2', 'sepallength2'], types=['string', 'float'])
ラテラルビューによる行出力と元の列の結合
reduce=False の場合、ラテラルビューを使用して UDF 出力を元の列と結合できます。これは集計に便利です。
>>> from odps.df import output
>>> @output(['iris_sub', 'iris_add'], ['float', 'float'])
>>> def handle(row):
>>> yield row.sepallength - row.sepalwidth, row.sepallength + row.sepalwidth
>>> yield row.petallength - row.petalwidth, row.petallength + row.petalwidth
>>> iris[iris.category, iris.apply(handle, axis=1)]
列へのカスタム集計の適用
apply と axis=0 (または axis 引数なし) を使用して、すべての Sequence オブジェクトにカスタム集計クラスを渡します。このクラスは buffer、__call__、merge、getvalue を実装する必要があります。
class Agg(object):
def buffer(self):
return [0.0, 0]
def __call__(self, buffer, val):
buffer[0] += val
buffer[1] += 1
def merge(self, buffer, pbuffer):
buffer[0] += pbuffer[0]
buffer[1] += pbuffer[1]
def getvalue(self, buffer):
if buffer[1] == 0:
return 0.0
return buffer[0] / buffer[1]>>> iris.exclude('name').apply(Agg)
sepallength_aggregation sepalwidth_aggregation petallength_aggregation petalwidth_aggregation
0 5.843333 3.054 3.758667 1.198667
UDF での MaxCompute リソースの読み取り
UDF は MaxCompute リソース (ファイルリソースとテーブルリソース) を読み取ることができます。また、Collection オブジェクトをリソースとして参照することもできます。リソースが行ごとではなく初期化時に一度だけロードされるように、UDF をクロージャまたは呼び出し可能クラスでラップしてください。
クロージャ内でリソースをロードすることで (関数呼び出しごとではなく)、繰り返しの初期化オーバーヘッドを回避できます。たとえば、ルックアップテーブルやモデルアーティファクトをロードする場合などです。
ファイルリソースと Collection リソースを使用した行レベル UDF
>>> file_resource = o.create_resource('pyodps_iris_file', 'file', file_obj='Iris-setosa')
>>> iris_names_collection = iris.distinct('name')[:2]
>>> iris_names_collection
sepallength
0 Iris-setosa
1 Iris-versicolor>>> def myfunc(resources): # リソースは呼び出し時の指定順で渡されます
>>> names = set()
>>> fileobj = resources[0] # ファイルリソースはファイルライクオブジェクトとして渡されます
>>> for l in fileobj:
>>> names.add(l)
>>> collection = resources[1]
>>> for r in collection:
>>> names.add(r.name) # フィールド名またはオフセットで値を取得します
>>> def h(x):
>>> if x in names:
>>> return True
>>> else:
>>> return False
>>> return h
>>> df = iris.distinct('name')
>>> df = df[df.name,
>>> df.name.map(myfunc, resources=[file_resource, iris_names_collection], rtype='boolean').rename('isin')]
>>> df
name isin
0 Iris-setosa True
1 Iris-versicolor True
2 Iris-virginica False
パーティション分割テーブルを読み取る場合、パーティションフィールドは含まれません。
ローカル DataFrame をリソースとして使用した行レベル UDF
ローカル変数は、実行時に MaxCompute のリソースとして参照できます。次の例では、stop_words はローカル DataFrame であり、実行プログラムがそれをリソースとして UDF に渡します。
>>> words_df
sentence
0 Hello World
1 Hello Python
2 Life is short I use Python
>>> import pandas as pd
>>> stop_words = DataFrame(pd.DataFrame({'stops': ['is', 'a', 'I']}))
>>> @output(['sentence'], ['string'])
>>> def filter_stops(resources):
>>> stop_words = set([r[0] for r in resources[0]])
>>> def h(row):
>>> return ' '.join(w for w in row[0].split() if w not in stop_words),
>>> return h
>>> words_df.apply(filter_stops, axis=1, resources=[stop_words])
sentence
0 Hello World
1 Hello Python
2 Life short use Python
行操作 (axis=1) の場合、関数クロージャまたは呼び出し可能クラスを使用してリソースをロードしてください。列集計の場合は、代わりに__init__メソッドを使用してください。
サードパーティ Python ライブラリのアップロード
MaxCompute は、.whl、.egg、.zip、.tar.gz 形式の Python パッケージのアップロードをサポートしています。すべての依存関係を明示的に指定する必要があります。依存関係を省略すると、実行時にインポートエラーが発生します。
パッケージの種類に応じて、アップロード方法を選択してください。
| パッケージの種類 | アップロード方法 | 備考 |
|---|---|---|
| プリインストール | 不要 | NumPy のみ |
| Pure Python パッケージ (コンパイル済みコードなし、ファイル操作なし) | .whl ファイルリソースとしてアップロード |
python-dateutil、pytz、six などのパッケージに対応。新しいバージョンの MaxCompute では、ファイル操作を含むパッケージもサポートされています。 |
| バイナリパッケージ (コンパイル済み C 拡張機能) | .zip アーカイブリソースとしてアップロードし、分離を有効化 |
cp27-cp27m-manylinux1_x86_64 プラットフォームタグが必要。Linux でビルドしてください。 |
Pure Python パッケージ
デフォルトでは、PyODPS は Pure Python コードを含むがファイル操作を含まないサードパーティライブラリをサポートしています。次の例では、python-dateutil とその依存関係である six をアップロードします。
ステップ 1:パッケージとその依存関係をダウンロードします。パッケージは Linux 用にビルドされている必要があります。
$ pip download python-dateutil -d /to/path/
これにより、six-1.10.0-py2.py3-none-any.whl と python_dateutil-2.5.3-py2.py3-none-any.whl がダウンロードされます。
ステップ 2:create_resource を使用して両方のファイルをリソースとしてアップロードします。
# ファイルの拡張子が正しいことを確認してください。
>>> odps.create_resource('six.whl', 'file', file_obj=open('six-1.10.0-py2.py3-none-any.whl', 'rb'))
>>> odps.create_resource('python_dateutil.whl', 'file', file_obj=open('python_dateutil-2.5.3-py2.py3-none-any.whl', 'rb'))
ステップ 3:ライブラリを使用します。options.df.libraries でセッション全体にグローバルに指定するか、libraries パラメータを実行メソッドに直接渡して適用範囲を単一の呼び出しに限定します。以下の例では、日付文字列を含むサンプル DataFrame を使用します。
>>> import pandas as pd
>>> from odps.df import DataFrame
>>> # 日付解析の例で使用するサンプル DataFrame を作成します
>>> df_dates = DataFrame(pd.DataFrame({'datestr': ['2016-03-01', '2015-10-10']}))
# グローバル設定 (このセッション内の以降のすべての DataFrame 操作に適用されます)
>>> from odps import options
>>> def get_year(t):
>>> from dateutil.parser import parse
>>> return parse(t).strftime('%Y')
>>> options.df.libraries = ['six.whl', 'python_dateutil.whl']
>>> df_dates.datestr.map(get_year)
datestr
0 2016
1 2015# 実行ごとの設定 (この呼び出しにのみ適用されます)
>>> def get_year(t):
>>> from dateutil.parser import parse
>>> return parse(t).strftime('%Y')
>>> df_dates.datestr.map(get_year).execute(libraries=['six.whl', 'python_dateutil.whl'])
datestr
0 2016
1 2015
バイナリパッケージ (コンパイル済みコードを含む)
コンパイル済み C 拡張機能を含むパッケージ (SciPy や pandas など) には、追加の手順が必要です。
-
.whlファイルはcp27-cp27m-manylinux1_x86_64プラットフォームタグを使用する必要があります。 -
ファイルをアーカイブリソースとしてアップロードし、
.whl拡張子を.zipに変更します。 -
odps.isolation.session.enableをTrueに設定するか、プロジェクト設定でisolationを有効にします。
# バイナリパッケージを .zip 拡張子のアーカイブとしてアップロードします。
>>> odps.create_resource('scipy.zip', 'archive', file_obj=open('scipy-0.19.0-cp27-cp27m-manylinux1_x86_64.whl', 'rb'))
# プロジェクトで既に分離が有効になっている場合、次のオプションは不要です。
>>> options.sql.settings = {'odps.isolation.session.enable': True}
>>> def psi(value):
>>> # オペレーティングシステム間の構造の違いによるエラーを避けるため、
>>> # サードパーティライブラリは関数内でインポートしてください。
>>> from scipy.special import psi
>>> return float(psi(value))
>>> df.float_col.map(psi).execute(libraries=['scipy.zip'])
ソースからバイナリパッケージをビルドするには、Linux シェルで次のコマンドを実行してください。macOS または Windows でビルドされた Wheel ファイルは MaxCompute と互換性がありません。
python setup.py bdist_wheel
MaxCompute コンソールによるアップロード
PyODPS API の代わりに、MaxCompute コンソールで add archive を使用してパッケージをアップロードすることもできます。
ステップ 1:各依存関係の正しいパッケージファイルを特定します。
ほとんどのパッケージは、複数のプラットフォーム用の .whl ファイルを提供しています。バイナリパッケージの場合、名前に cp27-cp27m-manylinux1_x86_64 が含まれるファイルを見つけてください。Pure Python パッケージの場合、py2.py3-none-any の Wheel が使用できます。
ステップ 2:必要なすべての依存関係を確認します。次の表は、一般的なパッケージの依存関係を示しています。
| パッケージ | 依存関係 |
|---|---|
| pandas | NumPy、python-dateutil、pytz、six |
| SciPy | NumPy |
| scikit-learn | NumPy、SciPy |
NumPy は既にプリインストールされています。python-dateutil、pytz、pandas、SciPy、scikit-learn、six のみをアップロードしてください。
ステップ 3:パッケージファイルをダウンロードします。次の表は、各パッケージでダウンロードする特定のファイルを示しています。
| パッケージ | ダウンロードするファイル | アップロードするリソース名 |
|---|---|---|
| python-dateutil | python-dateutil-2.6.0.zip | python-dateutil.zip |
| pytz | pytz-2017.2.zip | pytz.zip |
| six | six-1.11.0.tar.gz | six.tar.gz |
| pandas | pandas-0.20.2-cp27-cp27m-manylinux1_x86_64.zip | pandas.zip |
| SciPy | scipy-0.19.0-cp27-cp27m-manylinux1_x86_64.zip | scipy.zip |
| scikit-learn | scikit_learn-0.18.1-cp27-cp27m-manylinux1_x86_64.zip | sklearn.zip |
ステップ 4:各ファイルをアップロードします。バイナリパッケージ (pandas、SciPy、scikit-learn) の場合、アップロード前に .whl 拡張子を .zip に変更してください。
add archive python-dateutil.zip;
add archive pandas.zip;
実行時のライブラリの指定
options.df.libraries を使用してセッション全体にライブラリをグローバルに設定するか、libraries パラメータを実行メソッドに直接渡して適用範囲を単一の呼び出しに限定します。
# グローバル:このセッション内の以降のすべての DataFrame 操作に適用されます
>>> from odps import options
>>> options.df.libraries = ['six.whl', 'python_dateutil.whl']# ローカル:この実行呼び出しにのみ適用されます
>>> df.apply(my_func, axis=1).to_pandas(libraries=['six.whl', 'python_dateutil.whl'])