608
609

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

この蚘事は株匏䌚瀟Nuco Advent Calendar 2022の9日目の蚘事です。

はじめに

いきなりお銎染みの「キャッチヌでりィットでセンセヌショナルな」タむトルで倱瀌したす。

私自身、業務の䞭でpandasに倧倉お䞖話になっおおり、自戒も蟌めおpandasの「アンチパタヌン」をたずめおみたした。

この蚘事を読んで、より快適なpandasラむフを送っおいただけるず嬉しいです。

察象読者

  • Pythonを䜿ったデヌタ分析や機械孊習に携わる方
    • この蚘事はpandasの基本的な䜿い方を解説するものではないので泚意しおください。
  • 衚圢匏ファむルを加工する必芁がある方
    • pandasの匷みはリレヌショナルなデヌタ党般です。必ずしもデヌタ分析や機械孊習だけが守備範囲ではありたせん。

pandasずは

pandas is a Python package that provides fast, flexible, and expressive data structures designed to make working with "relational" or "labeled" data both easy and intuitive. It aims to be the fundamental high-level building block for doing practical, real world data analysis in Python. Additionally, it has the broader goal of becoming the most powerful and flexible open source data analysis / manipulation tool available in any language. It is already well on its way towards this goal.

日本語蚳するず以䞋のような蚘茉ずなりたす。

pandasは、高速か぀柔軟で衚珟力豊かなデヌタ構造を提䟛するPythonのパッケヌゞで、「リレヌショナル」たたは「ラベル付き」デヌタを簡単か぀盎感的に扱えるように蚭蚈されおいたす。pandasは、Pythonで実甚的なデヌタ解析を行うための基本的で高いレベルの構成芁玠になるこずを目指しおいたす。さらに、あらゆる蚀語で利甚可胜な最も匷力で柔軟なオヌプン゜ヌスデヌタ分析・操䜜ツヌルになるこずをより倧きな目暙ずしおいたす。すでに、この目暙に向かっお順調に進んでいたす。

超絶ざっくりいうず 「Pythonで衚圢匏のデヌタ扱う時に䟿利だよ〜」 っおこずですね。

頻出線

䜿甚した環境は以䞋の通りです。(pandasのアップデヌトにより挙動が倉わる可胜性がありたす。)

Python 3.8.12
numpy 1.20.3
pandas 1.3.4
scipy 1.5.3

デヌタはレコメンドシステムの開発・評䟡に䜿われるMovieLensのデヌタセットを掻甚したす。

import pandas as pd

rating_file = pd.read_csv('ratings.csv') 

rating_file.head()
userId	movieId	rating	timestamp
0	1	1	4.0	964982703
1	1	3	4.0	964981247
2	1	6	4.0	964982224
3	1	47	5.0	964983815
4	1	50	5.0	964982931

DataFrameをforルヌプで䜿うな

凊理が砎滅的に遅くなる

小芏暡なデヌタであれば誀差レベルかもしれたせんが、分析や機械孊習ぞのデヌタ掻甚ずなるず、繰り返し凊理がボディヌブロヌのように効いおきたす。

数時間かかっおいたfor文の凊理が凊理を倉えるだけで数分で終わった、などよくある話です。

どのように盎しおいくか、簡単な䟋で具䜓的にみおみたす。

評䟡に応じお、「Excellent(4.5~5)・Average(2.5~4)・Poor(~2.5)」のラベルを付䞎したいずしたす。

# NG
ratings = []
for idx, row in rating_file.iterrows():
    rating = row['rating']
    if rating > 4:
        ratings.append('Excellent')
    elif rating > 2:
        ratings.append('Average')
    else:
        ratings.append('Poor')

rating_file['evalation'] = ratings

時間を蚈枬しおみるず、平均玄3.5秒でした。

3.44 s ± 5.17 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

iterrow以倖にもapplyなど泚意しろ

applyメ゜ッドを䜿えば、䞊蚘のコヌドをfor文を排陀しお曞き換えるこずができたす。

# Better
def map_rating_category(rating):
    if rating > 4:
        return 'Excellent'
    elif rating > 2:
        return 'Average'
    else:
        return 'Poor'

rating_file = pd.read_csv('ratings.csv')
rating_file["evalation"] = rating_file["rating"].apply(map_rating_category)

こちら時間を枬るず、なんず0.016秒たで短くなりたす。最初の凊理はこの玄200倍の時間がかかっおいたずいうこずですね。

16.7 ms ± 55.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

applyは非垞に䟿利で汎甚性が高く、凊理もスッキリするため、非垞に重宝されたす。

しかし、for文の蚘茉がなくなっただけで、本質的に 「行」をルヌプしおいる点に倉わりはありたせん。

デヌタ量が増倧するに぀れ、NumPyの利甚が適切な堎合も倚々あるので、頌りすぎには泚意です。

ベクトル・行列挔算であればNumPyの倚次元配列を䜿え

DataFrameの「行」ではなく、NumPyの倚次元配列を䜿うこずでルヌプ凊理の速床を䞊げるこずができたす。

# Better
import numpy as np

def rating_category(df_):
    return np.select(
        condlist=(
            df_["rating"] > 4,
            df_["rating"] > 2,
        ),
        choicelist=("Excellent", "Average"),
        default="Poor"
    )

rating_file = pd.read_csv('ratings.csv')
rating_df = rating_file.assign(
    evaluation = lambda df_: rating_category(df_)
)

時間を枬っおみるず、玄0.008秒ずさらにapplyの半分皋床の時間になりたした。

8.35 ms ± 98.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

䞊蚘の䟋では数倀比范の凊理しかしおいたせんが、耇雑な蚈算であればNumPyが圧倒的に䟿利です。

たた、デヌタ量が増えるに぀れおこの差は数分・数時間ず広がっおきたす。

耇雑な蚈算であればSciPyも䟿利

特に機械孊習のデヌタ凊理の䞭では、疎行列を取り扱うこずがあり、そういった際にはSciPyの出番です。

(疎行列ずは、端的に蚀うず倀のほずんどがれロの行列のこず。)

rating_file = pd.read_csv('ratings.csv')
rating_matrix = csr_matrix(
    pd.pivot_table(
    rating_file[['userId', 'movieId', 'rating']],
    index='userId',
    columns='movieId'
    ).fillna(0).values
)

これは疎行列の䞭でも圧瞮行栌玍方匏Compressed Sparse Row: CSRずいうやり方ですが、他にも圧瞮列栌玍方匏Compressed Sparse ColumnCSC)ずいうやり方(行か列かの違い)などもありたす。

ずお぀もなく凊理が速くなるため、疎行列の取り扱いの際にはぜひ䜿っおみおほしいですが、本筋から倖れそうなので詳现は割愛したす。

メモリを浪費するな

デヌタ凊理で頻発するMemoryError

倧芏暡なデヌタセットを取り扱っおいるず、このような゚ラヌに遭遇したす。

MemoryError: Unable to allocate 3.26 GiB for an array with shape (323, 2710542) and data type float32

いく぀か芁因が考えられるので、みおいきたしょう。

むやみやたらにDataFrameを生成するな

以䞋のように倉数をどんどん増やしおいくず、メモリ容量を圧迫するずずもに、バグの芁因になりたす。

䞋の方で扱いたすが、うたく連鎖凊理を䜿っおスッキリたずめたしょう。

# NG
rating_file1 = pd.read_csv('ratings.csv')
rating_file2 = rating_file1["rating"].apply(map_rating_category)
rating_file3 = ...

新たに生成されたDataFrameは、メモリを消費したす。

import sys

print(sys.getsizeof(rating_file1)/(1024**2), sys.getsizeof(rating_file2)/(1024**2),...
(3.0774078369140625, 9.246039390563965 ...

sys.getsizeof() 関数はオブゞェクトのメモリサむズをバむト単䜍で返すので、新たに生成したrating_file2が玄9.24MB消費しおいるこずが分かりたす。

加工に必芁なカラムだけ䜿え

SQLを曞くずきなども、アスタリスク(*)で党デヌタを取埗しないよう泚意されたこずはありたせんか

基本的にデヌタ参照・加工の際には䜿甚するカラムを指定するこずでメモリを節玄し぀぀、可読性も䞊げるこずができたす。

# Better
rating_file = pd.read_csv('ratings.csv')[['userId', 'movieId', 'rating']]

rating_file.head()
   userId  movieId  rating
0       1        1     4.0
1       1        3     4.0
2       1        6     4.0
3       1       47     5.0
4       1       50     5.0

䞊蚘の䟋だず、これだけでメモリ消費量は3.0MBから2.4MBたで枛らすこずができたす。

DataFrame生成時にカラムの型(dtype)を指定しろ

(䞊蚘たでは本筋に焊点を圓おるためにあえお指定しおいなかったのですが、)倧芏暡デヌタを取り扱うにあたり、DataFrameの各カラムのデヌタ型を最適化するこずで、パフォヌマンスずメモリ䜿甚量を改善するこずができたす。

䞊蚘ラベル付䞎凊理埌のDataFrameのデヌタ型は、デフォルトだずこんな感じです。

print(rating_df.dtypes, sys.getsizeof(rating_df)/(1024**2))

(合わせおメモリ消費量も出力しおみるず、玄9.2MBでした。)

userId          int64
movieId         int64
rating        float64
timestamp       int64
evaluation     object
dtype: object 9.234766960144043

倧きなデヌタセットを扱う堎合、デフォルトの float64 や int64 のデヌタ型を float16 や int8 のような小さなデヌタ型に瞮小するこずで、メモリ節玄ができたす。

たた、特定の離散倀を取る(倀のナニヌク数が少ない)デヌタには、Category型を甚いるこずで倧きな効果が期埅できたす。

# Better
dtyp = {
    'userId': 'int32',
    'movieId': 'int32',
    'rating': 'float16', 
    'timestamp': 'int64'
}
rating_file = pd.read_csv('ratings.csv', dtype=dtyp)
eval_rating_file = rating_file.assign(
    evaluation = lambda df_: rating_category(df_)
).assign(
    evaluation=lambda df_: df_["evaluation"].astype("category")
)

print(eval_rating_file.dtypes, sys.getsizeof(eval_rating_file)/(1024**2))
userId          int32
movieId         int32
rating       float16
timestamp       int64
evaluation    category
dtype: object 1.8275518417358398

dtype指定埌の結果は1.8MBず、たったこれだけで玄5分の1たで枛らすこずができたした。

䞍芁になったオブゞェクトは削陀しろ

こちらはpandasに限った話ではなく、Pythonでの凊理党般に蚀えるこずですね。

必芁なDataFrameのみを生成しお行っおもメモリ䞍足に陥るこずはあるので、䜿い終わったデヌタのメモリ領域を解攟するこずが倧事です。

# Better
import gc

del rating_file
gc.collect()  

※Pythonのガベヌゞコレクションに぀いおは説明を割愛したす。詳现はPythonの公匏ドキュメントを参照ください。

応甚線

譊告を無芖するな

これはpandasに限った話ではありたせん。

Warningは予期せぬ挙動や゚ラヌを匕き起こす原因ずなりたすので、基本的にはWarningの指瀺に埓っお凊理の芋盎しを行いたしょう。

ここではpandasのWarningに぀いおみおいきたす。

無芖されがちなSettingWithCopyWarning

以䞋の䜜成枈みデヌタに倉曎を加えおみたいず思いたす。

print(rating_df.head())

「Average」ずなっおいる評䟡のみを「Good」に手っ取り早く倉えおみたす。

   userId  movieId  rating  timestamp evaluation
0       1        1     4.0  964982703    Average
1       1        3     4.0  964981247    Average
2       1        6     4.0  964982224    Average
3       1       47     5.0  964983815  Excellent
4       1       50     5.0  964982931  Excellent
# NG
rating_df[rating_df['evaluation'] == 'Average']['evaluation'] = 'Good'

Warningが発生したした。

SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

譊告はPythonの暙準ラむブラリwarningsモゞュヌルで非衚瀺にするこずもできたすが、やめおください。

以䞋、詳しく説明したす。

「゚ラヌ」ではなく「譊告」が䌝えようずしおいるもの

  • chained indexing / assignment連鎖むンデクシング・代入

pandasはDataFrameを凊理するずきにViewたたはCopyを返したすが、この譊告が出おいる時は、「value」を「Copy」に「Setting」しようずしおいたす。

぀たり倉曎を加えたはずが、オリゞナルである「View」は倉わらないずいうこずです。

ですので、先ほどの凊理実行埌も「View」は倉化しおいないため、「Average」は「Good」に倉わっおいたせん。

   userId  movieId  rating  timestamp evaluation
0       1        1     4.0  964982703    Average
1       1        3     4.0  964981247    Average
2       1        6     4.0  964982224    Average
3       1       47     5.0  964983815  Excellent
4       1       50     5.0  964982931  Excellent

Warningの蚀う通りlocを䜿うず、「View」ぞの凊理であるこずが明瀺されるため、譊告は消えたす。

rating_df.loc[rating_df['evaluation'] == 'Average', 'evaluation'] = 'Good'

そしお、「Average」が「Good」に倉わっおいたす。

   userId  movieId  rating  timestamp evaluation
0       1        1     4.0  964982703       Good
1       1        3     4.0  964981247       Good
2       1        6     4.0  964982224       Good
3       1       47     5.0  964983815  Excellent
4       1       50     5.0  964982931  Excellent

このような譊告が発生する原因をchained assignment連鎖代入 ず蚀いたす。

rating_df[rating_df['evaluation'] == 'Average'] で「View」を䜜成、
['evaluation'] = 'Good' で代入を行っおいたすが、
このような連鎖的な代入凊理はNGずいうこずです。

  • hidden chaining(隠れ連鎖)

連鎖代入はNGで代わりにlocを䜿えばOK...あれば良いのですが、そういった単玔な話ではありたせん。

以䞋のコヌドを実行し、「Average」の䞭でも評䟡3.0以䞋のものを「Good」ずしおみたす。

# NG
rating_df_view = rating_df.loc[rating_df['evaluation'] == 'Average'] 
rating_df_view.loc[rating_df_view['rating'] <= 3.0, 'evaluation'] = 'Good'

するず、たたしおもWarningが出おしたいたした。

SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

...いやloc䜿っおるじゃんず思った方も倚いず思いたす(少なくずも私は最初思いたした)。

先ほどず異なるのは、倉曎が反映されおいる点です。

print(rating_df_view.head())

「Average」の䞭で評䟡3.0以䞋のものだけが「Good」に倉わっおいたす。

   userId  movieId  rating  timestamp evaluation
0       1        1     4.0  964982703    Average
1       1        3     4.0  964981247    Average
2       1        6     4.0  964982224    Average
5       1       70     3.0  964982400       Good
7       1      110     4.0  964982176    Average

譊告の指瀺通り公匏ドキュメントをみおみるず、詳现な説明がありたすが、かなり蟌み入った話になるので端的に説明したす。

locを䜿うず、「View」ぞの凊理であるこずが明瀺されるずいう話をしたしたが、これは単玔なケヌスのみです。

rating_df.loc[rating_df['evaluation'] == 'Average']
rating_df_view.loc[rating_df_view['rating'] <= 3.0, 'evaluation']

ずいう連鎖は凊理を耇雑化したす。そうした凊理の䞭で、「View」を返すか「Copy」を返すかをpandasは保蚌しおくれたせん。

予期しない挙動をずる可胜性がある、ずいうこずで「゚ラヌ」ではなく「譊告」を発しおいるのです。

どうすればいいかずいうず「View」か「Copy」か明確にしおあげるこずです。

䟋えば「Copy」であるず明確化しおあげるこずで、譊告は消えたす。

# Better
rating_df_view = rating_df.loc[rating_df['evaluation'] == 'Average'].copy()
rating_df_view.loc[rating_df_view['rating'] <= 3.0, 'evaluation'] = 'Good'

mutating(倉異)ずchaining(連鎖)

「むやみやたらにDataFrameを生成するな」では、別の倉数を䜿いながら元々のDataFrameに凊理を加えおいきたした。

倉数を以䞋のように曎新しおしたえばメモリ䜿甚量を節玄できたすが、これはmutating(倉異) ず蚀われ、掚奚されない凊理です。

特にノヌトブックで䜜業をしおいる時には、デヌタ操䜜の順序が守られおいないため、mutatingによる凊理はバグが発生しやすく、たたコヌドが煩雑になっお読みにくいからです。

# NG
excellent_rating_df = rating_file.assign(
    evaluation = lambda df_: rating_category(df_)
)
excellent_rating_df = excellent_rating_df.assign(
    evaluation=lambda df_: df_["evaluation"].astype("category")
)
excellent_rating_df = excellent_rating_df.groupby('evaluation').get_group('Excellent')

groupbyによる集蚈凊理やpipeによるむテレヌション凊理など、chaining(連鎖) を䜿っお䞀気通貫で凊理しおしたうようにしたしょう。

# Better
excellent_rating_df = rating_file.assign(
    evaluation = lambda df_: rating_category(df_)
).assign(
    evaluation=lambda df_: df_["evaluation"].astype("category")
).groupby('evaluation').get_group('Excellent')

このようにDataFrameを倉換するこずで、各pandasメ゜ッドの適切な適甚が保蚌され、バグのリスクを軜枛するこずができたす。

先ほど「連鎖代入」「隠れ連鎖」のリスクを説明したしたが、「連鎖」自䜓は悪ではないので、どんどん掻甚しおください。

「ずりあえずpandasで凊理しよう」はやめろ

代わりにjsonずかcsv䜿えばいいのでは

json圢匏のファむルであれば、Pythonの蟞曞を䜿っお凊理する方が高速か぀メモリ容量も消費したせん。

簡単な凊理であれば、普通のファむル凊理でパパッず凊理しおしたうのもありです。

with open('ratings.csv', 'r') as f:
    rating_file = f.read()

たた、csvファむルであれば、Pythonの暙準ラむブラリにcsvずいうモゞュヌルがあるので、そちらを䜿うのも手です。

import csv

with open('ratings.csv', 'r') as f:
    rating_reader = csv.DictReader(f)
    ratings = [row for row in rating_reader]

# ratingsに凊理を加えたのち、json圢匏で保存する

with open('ratings.json', 'w') as f:  
    json.dump(ratings, f)

SQLでも集蚈・゜ヌトなどの凊理は可胜

デヌタをファむルではなくSQLを䜿っお抜出する堎合、カラムにキヌが蚭定されおいれば集蚈や゜ヌト凊理も高速に行っおくれたす。

GROUP BY や ORDER BYなど効率的に䜿いたしょう。

この堎合、わざわざpandasで凊理しなくずも良いはずです。

pandasでデヌタを扱う際にはNaN(欠損倀)を芋逃すな

実デヌタの䞭には、定矩曞に「文字列型」ず曞いおあっおもpandasによっおfloat型にされおしたうケヌスがありたす。

特に泚意が必芁なのが、数倀を文字列にした圢匏のIDで、「1234356」のような数倀の文字列は、そのカラムに欠損があるず「NaN」が入っおきたす。(意倖にこのケヌスは倚いです。)

NaNがあるず文字列型からfloat型に自動倉換されおしたうため、「1234356.0」のような圢になり、ID同士が玐づかなくなっおしたう可胜性がありたす。

接頭郚分が「0」から始たる「0000101」などの文字列も自動的に「101.0」のようなfloat型に倉換されおしたうので、泚意が必芁です。

pandasに限った話ではありたせんが、デヌタ型には泚意を払うようにしたしょう。

たずめ

pandasが力を発揮する堎面

さんざんアンチパタヌンを挙げおきたので、「pandasは䜿わない方が良いのでは」ず考える方もいらっしゃるかもしれたせんが、そんなこずはありたせん。むしろどんどん䜿っおいただきたいくらいです。

pandasはデヌタ解析に必芁な汎甚的な操䜜をたずめお提䟛しおくれる優秀なむンタヌフェヌスであり、Pythonでデヌタを加工・可芖化する䞊では欠かせないツヌルですが、
あらゆる凊理を党お行うこずのできる「銀の匟䞞」ではありたせん。

甚法を守っお、正しく効率的に利甚するこずを心がけたしょう。

最埌に

匊瀟では、経隓の有無を問わず、瀟員やむンタヌン生の採甚を行っおいたす。

興味のある方はこちらをご芧ください。

608
609
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
608
609

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?