DataFrame을 Validation pandera 시작하기

개시하다


파이톤을 사용하여 데이터 분석을 할 때 자주 사용하는 프로그램 라이브러리에pandas가 있습니다.
판다스는 매우 사용하기 좋은 프로그램 라이브러리이지만 데이터 전체를 저장하기 위해pd.DataFrame형은 원본 코드만 보면'어떤 열이 있나','어떤 열이 어떤 유형인가','열별 값이 어떤 값이 있을까'등을 모르는 경우가 많다.
그 결과 처리 블랙박스화로 인해 디버깅 비용이 증가하고 코드의 가독성이 떨어지는 등 문제가 발생했다.
이 문제를 해결하는 방법 중 하나로서 본 글은 데이터 프레임 검증 기능을 제공하는 프로그램 라이브러리pandera를 소개한다.

이른바 판다라


데이터 처리 파이프의 가독성과 루팡성을 높이기 위해 데이터 검증 기능을 제공하는 프로그램 라이브러리.
https://github.com/pandera-dev/pandera
https://pandera.readthedocs.io/en/stable/
주로 다음과 같은 기능을 제공합니다.(위의 문서에서 참조합니다.부분 발췌문)
  • 정의 모드를 통해 각종 데이터 프레임의 유형을 검증할 수 있음
  • 데이터 프레임의 열과 값을 검사할 수 있음
  • 클래스 기반 API(예를 들어pydantic)를 통해 모델 모델을 정의할 수 있다
  • 파이톤 도구와 통합 가능
  • pydantic,fastapi,mypy)
    개인적으로pydantic처럼 학급 기반의 API로 모델을 정의할 수 있다는 점은 고맙다고 생각합니다.

    설치하다.


    pip로 설치할 수 있습니다.
    pip install pandera
    

    사용법


    DataFramSchema의 발리에 따르면


    정부 강좌에서 발췌하다.
    import pandas as pd
    import pandera as pa
    
    # バリデーション用のデータ
    df = pd.DataFrame({
        "column1": [1, 4, 0, 10, 9],
        "column2": [-1.3, -1.4, -2.9, -10.1, -20.4],
        "column3": ["value_1", "value_2", "value_3", "value_2", "value_1"],
    })
    
    # スキーマ定義
    schema = pa.DataFrameSchema({
        "column1": pa.Column(int, checks=pa.Check.le(10)),
        "column2": pa.Column(float, checks=pa.Check.lt(-1.2)),
        "column3": pa.Column(str, checks=[
            pa.Check.str_startswith("value_"),
            # series の入力を受け取り boolean か boolean 型の series を返すカスタムチェックメソッドを定義
            pa.Check(lambda s: s.str.split("_", expand=True).shape[1] == 2)
        ]),
    })
    
    validated_df = schema(df)
    print(validated_df)
    
       column1  column2  column3
    0        1     -1.3  value_1
    1        4     -1.4  value_2
    2        0     -2.9  value_3
    3       10    -10.1  value_2
    4        9    -20.4  value_1
    
    는 사전에 모델을 정의하고 모델에 데이터 프레임을 입력한 후 검증된 데이터 프레임을 출력한다.

    SchemaModel 기반 검증


    이제 Schema Model을 사용하는 방법을 살펴보겠습니다.이것도 교과서에서 발췌한 것이다.
    from pandera.typing import Series
    
    class Schema(pa.SchemaModel):
    
        column1: Series[int] = pa.Field(le=10)
        column2: Series[float] = pa.Field(lt=-1.2)
        column3: Series[str] = pa.Field(str_startswith="value_")
    
        @pa.check("column3")
        def column_3_check(cls, series: Series[str]) -> Series[bool]:
            """Check that column3 values have two elements after being split with '_'"""
            return series.str.split("_", expand=True).shape[1] == 2
    
    Schema.validate(df)
    
    맞춤형 검사 방법은 lambda가 아니라 캠코더를 끼는 방법으로 실시되었지만 쓰기 방법은 큰 차이가 없다.
    또한 pandera.Field에서 얻은 주요 매개 변수는 다음과 같은 몇 가지가 있다.
    매개 변수
    설명
    nullable
    빈 열 허용 여부
    unique
    열에 고유한 제한을 적용할지 여부
    coerce
    강제 유형
    ignore_na
    형식 검사 중null 무시 여부
    eq
    지정한 값과 같거나 같음
    ge
    지정된 값보다 큽니다.
    gt
    지정된 값보다 크거나 같음
    le
    보다 작음
    lt
    지정된 값보다 낮음
    ne
    요소가 있는지 없는지
    in_range
    지정된 최소값 또는 최대값 범위 내
    isin
    지정된 목록 범위 내
    str_contains
    지정한 문자열 포함 여부
    str_startswith
    지정한 문자열부터 시작하기
    str_endswith
    지정한 문자열로 끝냅니까
    str_length
    문자 길이에 지정된 최소값 또는 최대값 범위 내

    실천적 사용 방법


    사용법을 간단히 이해했더라도 타이타닉의 데이터 세트를 읽어 가공 처리해 봤다.
    https://www.kaggle.com/competitions/titanic/overview

    데이터 읽기


    우선 패턴 정의를 하지 않는 상황을 고려한다.
    import pandas as pd
    
    
    def load_data(filepath: str) -> pd.DataFrame:
        df = pd.read_csv(filepath)
        return df
    
    
    df = load_data("./train.csv")
    
    파일에서 데이터를 읽는 것은 당연하지만 데이터 프레임의 내용이 어떤지 모른다.
    이어서 정의 모델의 상황을 고려한다.
    from typing import Optional
    
    import pandas as pd
    import pandera as pa
    from pandera.typing import Series, DataFrame
    
    
    class TitanicSchema(pa.SchemaModel):
        PassengerId: Series[int] = pa.Field(nullable=False, unique=True)
        Survived: Optional[Series[int]] = pa.Field(nullable=True, isin=(0, 1))
        Pclass: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3))
        Name: Series[str] = pa.Field(nullable=False)
        Sex: Series[str] = pa.Field(nullable=False, isin=("male", "female"))
        Age: Series[float] = pa.Field(nullable=True, in_range={"min_value": 0, "max_value": 100})
        SibSp: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Parch: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Ticket: Series[str] = pa.Field(nullable=False)
        Fare: Series[float] = pa.Field(nullable=True, ge=0)
        Cabin: Series[str] = pa.Field(nullable=True)
        Embarked: Series[str] = pa.Field(nullable=True, str_length=1, isin=("S", "C", "Q"))
        
        class Config:
            strict = True
    
    
    def load_dataset(filepath:str) -> DataFrame[TitanicSchema]:
        df = pd.read_csv(filepath)
        df = TitanicSchema.validate(df)
        return df
    
    
    df = load_data("./train.csv")
    
    데이터 집합이 가지고 있는 열과 각 열의 정보는 모델로 정의되기 때문에 데이터 프레임의 내용이 어느 정도 명확해졌다.
    또한 읽기 후 바로 검증을 실시하여 데이터 프레임이 모델로 정의된 내용을 만족시켰음을 확보했다.

    보태다


    위의 예에서 TitanicSchema 클래스의 구성원 변수에 Config 클래스를 정의하고 설정strict=True.
    이렇게 하면 pa.SchemaModel 등록Config류를 통해 원 정보를 정의할 수 있다.
    기본적으로 SchemaModel에 등록된 열이 존재하지 않으면 검증에서 오류가 발생하지만 등록되지 않은 열이 있어도 오류가 발생하지 않습니다.
    등록된 열이 없는 경우에도 오류가 발생하도록 설정strict=True합니다.

    데이터 가공


    다음은 데이터를 가공해 보자.가공 과정은 다음과 같은 notebook을 참고했다.
    이 글에서 가공 처리 자체는 중요하지 않으니 걸어가면서 읽어도 괜찮습니다.
    https://www.kaggle.com/code/startupsci/titanic-data-science-solutions
    아까와 마찬가지로 패턴을 정의하지 않는 상황을 먼저 고려한다.
    import numpy as np
    import pandas as pd
    
    
    def transform(df: pd.DataFrame) -> pd.DataFrame:
        df["Sex"] = df["Sex"].str.match("male").map(int)
    
        df["Title"] = df["Name"].str.extract(" ([A-Za-z]+)\.", expand=False)
        df["Title"] = df["Title"].replace(
            ["Lady", "Countess", "Capt", "Col", "Don", "Dr", "Major", "Rev", "Sir", "Jonkheer", "Dona"], "Rare"
        )
        df["Title"] = df["Title"].replace("Mlle", "Miss")
        df["Title"] = df["Title"].replace("Ms", "Miss")
        df["Title"] = df["Title"].replace("Mme", "Mrs")
        df["Title"] = df["Title"].map({"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5})
    
        guess_ages = np.zeros((2, 3))
        for i in range(2):
            for j in range(3):
                guess_df = df[(df["Sex"] == i) & (df["Pclass"] == j + 1)]["Age"].dropna()
                age_guess = guess_df.median()
                guess_ages[i, j] = int(age_guess / 0.5 + 0.5) * 0.5
        for i in range(2):
            for j in range(3):
                df.loc[(df["Age"].isnull()) & (df["Sex"] == i) & (df["Pclass"] == j + 1), "Age"] = guess_ages[i, j]
        df["Age"] = df["Age"].astype(int)
    
        df.loc[df["Age"] <= 16, "Age"] = 0
        df.loc[(df["Age"] > 16) & (df["Age"] <= 32), "Age"] = 1
        df.loc[(df["Age"] > 32) & (df["Age"] <= 48), "Age"] = 2
        df.loc[(df["Age"] > 48) & (df["Age"] <= 64), "Age"] = 3
        df.loc[df["Age"] > 64, "Age"] = 4
    
        df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
    
        df["IsAlone"] = df["FamilySize"].map(lambda x: 1 if x == 1 else 0)
    
        df = df.drop(["Ticket", "Cabin", "PassengerId", "Name"], axis=1)
    
        return df
    
    
    df = load_data("./train.csv")
    df = transform(df)
    
    가공은 결손 보완, 합병, 유형 전환, 각 열 연산, 무용열 삭제 등 각종 처리를 포함한다.
    나는 이러한 처리 결과를 통해 최종적으로 얻을 수 있는 데이터 프레임이 어떤 상태로 변하는지 곧 이해하기 어렵다고 생각한다.
    이어서 정의 모델의 상황을 고려한다.
    class TransformedTitanicSchema(pa.SchemaModel):
        Survived: Optional[Series[int]] = pa.Field(nullable=True, isin=(0, 1))
        Pclass: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3))
        Sex: Series[int] = pa.Field(nullable=False, isin=(0, 1))
        Age: Series[int] = pa.Field(nullable=False, isin=(0, 1, 2, 3, 4))
        SibSp: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Parch: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Fare: Series[float] = pa.Field(nullable=True, ge=0)
        Embarked: Series[str] = pa.Field(nullable=True, str_length=1, isin=("S", "C", "Q"))
        Title: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3, 4, 5))
        FamilySize: Series[int] = pa.Field(nullable=False, ge=0, le=15)
        IsAlone: Series[int] = pa.Field(nullable=False, isin=(0, 1))
        
        class Config:
            strict = True
        
    
    def transform(df: DataFrame[TitanicSchema]) -> DataFrame[TransformedTitanicSchema]:
        df["Sex"] = df["Sex"].str.match("male").map(int)
    
        df["Title"] = df["Name"].str.extract(" ([A-Za-z]+)\.", expand=False)
        df["Title"] = df["Title"].replace(
            ["Lady", "Countess", "Capt", "Col", "Don", "Dr", "Major", "Rev", "Sir", "Jonkheer", "Dona"], "Rare"
        )
        df["Title"] = df["Title"].replace("Mlle", "Miss")
        df["Title"] = df["Title"].replace("Ms", "Miss")
        df["Title"] = df["Title"].replace("Mme", "Mrs")
        df["Title"] = df["Title"].map({"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5})
    
        guess_ages = np.zeros((2, 3))
        for i in range(2):
            for j in range(3):
                guess_df = df[(df["Sex"] == i) & (df["Pclass"] == j + 1)]["Age"].dropna()
                age_guess = guess_df.median()
                guess_ages[i, j] = int(age_guess / 0.5 + 0.5) * 0.5
        for i in range(2):
            for j in range(3):
                df.loc[(df["Age"].isnull()) & (df["Sex"] == i) & (df["Pclass"] == j + 1), "Age"] = guess_ages[i, j]
        df["Age"] = df["Age"].astype(int)
    
        df.loc[df["Age"] <= 16, "Age"] = 0
        df.loc[(df["Age"] > 16) & (df["Age"] <= 32), "Age"] = 1
        df.loc[(df["Age"] > 32) & (df["Age"] <= 48), "Age"] = 2
        df.loc[(df["Age"] > 48) & (df["Age"] <= 64), "Age"] = 3
        df.loc[df["Age"] > 64, "Age"] = 4
    
        df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
    
        df["IsAlone"] = df["FamilySize"].map(lambda x: 1 if x == 1 else 0)
    
        df = df.drop(["Ticket", "Cabin", "PassengerId", "Name"], axis=1)
        
        df = TransformedTitanicSchema.validate(df)    
        
        return df 
    
    
    df = load_data("./train.csv")
    df = transform(df)
    
    가공 처리에는 변화가 없지만 처리 내용을 완전히 이해하지 못하더라도 어느 정도 어떤 값이 나올지 알 수 있다.
    또한 가공 처리가 끝난 후 바로 데이터의 검증이 이루어졌기 때문에 가공 후 예상치 못한 값이 섞이지 않도록 보증할 수 있다.

    읽기 및 가공


    총괄은 아래와 같다.
    또한 상기 원본 코드는 방법의 마지막 실행SchemaModel.validate에서 유형 확인을 했지만 매번 쓰기가 번거롭다.
    유형 알림이 있는 방법에 도매기@pa.check_types를 추가하면 자동으로 검증할 수 있다.
    import numpy as np
    import pandas as pd
    import pandera as pa
    from typing import Optional
    from pandera.typing import Series, DataFrame
    
    
    class TitanicSchema(pa.SchemaModel):
        PassengerId: Series[int] = pa.Field(nullable=False, unique=True)
        Survived: Optional[Series[int]] = pa.Field(nullable=True, isin=(0, 1))
        Pclass: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3))
        Name: Series[str] = pa.Field(nullable=False)
        Sex: Series[str] = pa.Field(nullable=False, isin=("male", "female"))
        Age: Series[float] = pa.Field(nullable=True, in_range={"min_value": 0, "max_value": 100})
        SibSp: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Parch: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Ticket: Series[str] = pa.Field(nullable=False)
        Fare: Series[float] = pa.Field(nullable=True, ge=0)
        Cabin: Series[str] = pa.Field(nullable=True)
        Embarked: Series[str] = pa.Field(nullable=True, str_length=1, isin=("S", "C", "Q"))
    
        class Config:
            strict = True
    
    
    class TransformedTitanicSchema(pa.SchemaModel):
        Survived: Optional[Series[int]] = pa.Field(nullable=True, isin=(0, 1))
        Pclass: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3))
        Sex: Series[int] = pa.Field(nullable=False, isin=(0, 1))
        Age: Series[int] = pa.Field(nullable=True, isin=(0, 1, 2, 3, 4))
        SibSp: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Parch: Series[int] = pa.Field(nullable=False, ge=0, le=10)
        Fare: Series[float] = pa.Field(nullable=True, ge=0)
        Embarked: Series[str] = pa.Field(nullable=True, str_length=1, isin=("S", "C", "Q"))
        Title: Series[int] = pa.Field(nullable=False, isin=(1, 2, 3, 4, 5))
        FamilySize: Series[int] = pa.Field(nullable=False, ge=0, le=15)
        IsAlone: Series[int] = pa.Field(nullable=False, isin=(0, 1))
    
        class Config:
            strict = True
    
    
    @pa.check_types
    def load_dataset(filepath: str) -> DataFrame[TitanicSchema]:
        df = pd.read_csv(filepath)
        return df
    
    
    @pa.check_types
    def transform(df: DataFrame[TitanicSchema]) -> DataFrame[TransformedTitanicSchema]:
        df["Sex"] = df["Sex"].str.match("male").map(int)
    
        df["Title"] = df["Name"].str.extract(" ([A-Za-z]+)\.", expand=False)
        df["Title"] = df["Title"].replace(
            ["Lady", "Countess", "Capt", "Col", "Don", "Dr", "Major", "Rev", "Sir", "Jonkheer", "Dona"], "Rare"
        )
        df["Title"] = df["Title"].replace("Mlle", "Miss")
        df["Title"] = df["Title"].replace("Ms", "Miss")
        df["Title"] = df["Title"].replace("Mme", "Mrs")
        df["Title"] = df["Title"].map({"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5})
    
        guess_ages = np.zeros((2, 3))
        for i in range(2):
            for j in range(3):
                guess_df = df[(df["Sex"] == i) & (df["Pclass"] == j + 1)]["Age"].dropna()
                age_guess = guess_df.median()
                guess_ages[i, j] = int(age_guess / 0.5 + 0.5) * 0.5
        for i in range(2):
            for j in range(3):
                df.loc[(df["Age"].isnull()) & (df["Sex"] == i) & (df["Pclass"] == j + 1), "Age"] = guess_ages[i, j]
        df["Age"] = df["Age"].astype(int)
    
        df.loc[df["Age"] <= 16, "Age"] = 0
        df.loc[(df["Age"] > 16) & (df["Age"] <= 32), "Age"] = 1
        df.loc[(df["Age"] > 32) & (df["Age"] <= 48), "Age"] = 2
        df.loc[(df["Age"] > 48) & (df["Age"] <= 64), "Age"] = 3
        df.loc[df["Age"] > 64, "Age"] = 4
    
        df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
    
        df["IsAlone"] = df["FamilySize"].map(lambda x: 1 if x == 1 else 0)
    
        df = df.drop(["Ticket", "Cabin", "PassengerId", "Name"], axis=1)
    
        return df
    
    
    def main():
        df = load_dataset("./train.csv")
        df = transform(df)
    
    
    if __name__ == "__main__":
        main()
    
    

    끝맺다


    본고는 데이터 프레임 검증 기능을 제공하는 프로그램 라이브러리pandera를 소개한다.
    단순히 원본 코드의 분량을 보면 배 가까이 증가한다. 원가 없이 검증과 유형 검사를 할 수 없기 때문에 짧은 시간 안에 폐기되는 전제에서 원본 코드는 사용할 가치가 없을 수 있다.여러 사람이 장기적으로 유지보수해야 하는 원본 코드는 매우 효과적이라고 느낀다.
    기계 학습 모델이 생산 환경에서 작동하는 것은 당연한 일이기 때문에 데이터클래스와pydantic 등 모델에 대한 정의, 도입 유형 알림을 개발하는 기업이 증가했다.데이터 프레임에 유형 힌트와 유형 검사를 추가하지 못해 곤란하다면pandera를 사용하는 것도 방법을 연구해 보고 싶습니다.
    그때 기사에 도움이 된다면 영광입니다.끝까지 읽어주셔서 감사합니다.

    참고 자료


    창고.
    https://github.com/pandera-dev/pandera
    문서
    https://pandera.readthedocs.io/en/stable/
    일본어 소개
    https://ohke.hateblo.jp/entry/2020/09/12/230000
    https://qiita.com/kamok0731/items/06f224d45e17e9c4aa85

    좋은 웹페이지 즐겨찾기