web-dev-qa-db-ja.com

解釈可能/保守可能の設計python pandas DataFrames?

python using pandas dataframes)でかなりの量のコードを操作/作成しています。私が実際に苦労していることの1つは、「スキーマ"並べ替えるか、データフレーム内のデータフィールドを明確にします。たとえば、

次の列を持つデータフレームdfがあるとします

customer_id | order_id | order_amount | order_date | order_time |

これで、get_average_order_amount_per_customerという関数ができました。これは、顧客ごとの列order_amountの平均を取得するだけです。グループのように

def get_average_order_amount_per_customer(df):
    df = df.groupby(['customer_id']).mean()
    return pd.DataFrame(df['order_amount'])

数週間後、この関数を見てみると、customer_idorder_amount以外の何がこのデータフレーム内にあるのかわかりません。そのDFを使用する前処理ステップを見て、order_idorder_dateorder_timeを使用する他の関数を見つけたいと思います。これは時々処理/使用状況を、それが発生したファイル/データベーススキーマまでさかのぼって追跡する必要があります。データフレームが強く型付けされているか、印刷してチェックせずにコードに表示されるスキーマがあったら、ログなので、どの列にあるかを確認し、必要に応じて名前を変更したり、クラスの場合と同じようにフィールドにデフォルト値を追加したりできます。

クラスのように、Orderオブジェクトを作成し、そこに必要なフィールドを配置するだけで、Orderクラスファイルをチェックして、使用可能なフィールドを確認できます。

一部のコードはデータフレームを入力として使用しているため、データフレームをまとめて取り除くことができません。

Python Typing libraryを使用しています。データフレーム内のスキーマに名前を付けることはできません。

では、データフレームコンテンツのあいまいさのこのハードルを克服できるようにする、私が従うことができるデザインパターンやテクニックはありますか?

4
alex

私の最初のアイデアは、pandas DataFrameのロードを担当する関数にタイプヒントと説明的なdocstringを含めることです。例:

_import pandas as pd


def load_client_data(input_path: str) -> pd.DataFrame:
    """Loads client DataFrame from a csv file, performs data preprocessing and returns it.

    The client DataFrame is formatted as follows:

    costumer_id (string): represents a customer.
    order_id (string): represents an order.
    order_amount (int): represents the number of items bought.
    order_date (string): the date in which the order was made (YYYY-MM-DD).
    order_time (string): the time in which the order was made (HH:mm:ss)."""

    client_data = pd.read_csv(input_path)
    preprocessed_client_data = do_preprocessing(client_data)
    return preprocessed_client_data
_

理想的には、データセットのロードを担当するすべての関数をモジュールにまとめてバンドルし、少なくとも疑わしいときはいつでもどこを見ればよいかがわかるようにします。データセットの適切で一貫性のある変数名は、ダウンストリーム関数で使用しているデータセットを追跡するのにも役立ちます。

もちろん、これによりcouplingが少し追加されます:データセットの列を変更する場合は、 docstringも更新することを忘れないでください。ただし、結局のところ、それは柔軟性と信頼性のどちらかを選択することになります。プログラムのサイズが大きくなり、安定すると、それはかなり妥協だと思います。

また、データセット自体に対する操作(新しい列の追加、日付の日/月/年の列への解析など)をできるだけ早く実行して、docstringがこれらのメモリ内の変更も反映するようにします。データセットが別の関数でずっと変換されている場合は、もっと早く変換できるかどうか自問してください。それが不可能な場合は、少なくとも、将来のデータを予期する空の列でデータフレームを初期化し、この情報をdocstringに反映します。

これをさらに一歩進めたい場合は、データセットのロードに関連するすべての関数をDatasetManagerクラスにラップして、データセットの署名の情報を統合できます。ヘルパー関数を追加して、特定のデータセットのdocstringをすばやく表示することもできます。たとえば、dataset_manager.get_info('client_data')と書くと、_load_client_data_関数のdocstringを出力できます。

最後に、それが問題なければ、pandas DataFramesにデータ型を適用するのに役立ついくつかのサードパーティモジュールがあります。例は dataenforce ですが、免責事項として、私はそれを個人的に使用したことがありません。

1
jfaccioni

データを検証する関数を記述し、必要な関数を装飾できます。

テストをpython関数として表現できる場合:

def has_columns(df, columns):
    """
    Checks whether all `columns` are in `df`

    Retuns a boolean result and the missing columns
    """
    if isinstance(columns, str):
        # to prevent the later `set` command from mangling our string
        columns = {columns}
    return set(df) >= set(columns), set(columns) - set(df)

次に、デコレータを作成できます。

from functools import wraps
def has_columns_decorator(columns, df_name="df"):
    """
    Checks for presence of `columns` in an argument to the decorated function

    Expects a function with a DataFrame as keyword argument `df_name` 
    or as first argument

    Checks whether all `columns` are columns in the DataFrame
    Raises a ValueError if the check fails
    """
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if df_name in kwargs:
                df = kwargs.pop(df_name)
            else:
                df, *args = args
            check_result, missing_columns = has_columns(df, columns)
            if not check_result:
                raise ValueError(
                    f"Not all columns are present: {missing_columns}"
                )

            result = func(*args, df=df, **kwargs)
            return result
        return wrapper
    return decorate

この検証を好きなだけ複雑にして、dtypeをチェックしたり、0より大きいかどうかなどを確認したりできます。

次のように使用できます。

@has_columns_decorator("a")
def my_func(df):
    return df

my_func(df)
      a   b
0     0   a
1     1   b
2     2   c
​
@has_columns_decorator(["a", "c"])
def my_func2(df):
    return df

my_func2(df)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-53-997c72a8b535> in <module>
----> 1 my_func2(df)

<ipython-input-50-36b8ff709aa9> in wrapper(*args, **kwargs)
     28             if not check_result:
     29                 raise ValueError(
---> 30                     f"Not all columns are present: {missing_columns}"
     31                 )
     32 

ValueError: Not all columns are present: {'c'}

必要に応じてチェックを入念に行うことができます。 engarde は、すでにいくつかのチェックが行われているパッケージです

1
Maarten Fabré

。dtypes または個別のdf.column.dtypeをアサートできます。

または、 。astype() を使用して列のデータ型を変換しますが、使用可能な列とそれらの列のタイプを定義することで、データのスキーマを文書化することもできます。 dtypesは、データフレームコンストラクターでも指定できます。

0
Lie Ryan