wellwell3176
@wellwell3176

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

pandasデータフレームから型がdatetimeの行だけ抜き取りたい

解決したいこと

エクセルファイルをpandasに読み取った時、下表のようにデータ内に予期しないメモ書きなどが混ざる場合がある。

日付 動物
1/1 ネズミ
1/3 ウサギ
1/4 コアラ
餅が食べたい 私も食べたい
1/4 ゴリラ

日付列を型判定し、datetimeの型だけを取出して下記のようにしたい
全行に対してisinstanceを使えば実現はできたのですが(『自分で試したこと』参照)、
データ量が多くなった時を考えるとあまり良い方法とは思えず、他に方法があれば教えていただきたく。

日付 動物
1/1 ネズミ
1/3 ウサギ
1/4 コアラ
1/4 ゴリラ

自分で試したこと

bool列を生成し、isinstance関数を回して「日付列がdatetime型かどうか」を判定。
bool列がTrueのところだけ取り出す。

自作
  df["bool"]=""
  for i in range(len(df["bool"])):
    df.at[i,"bool"]=isinstance(df_raw.at[i,"日付"],datetime.datetime) #下表はこの時点でのもの
  df=df[(df["bool"]==True)]

日付 動物 bool
1/1 ネズミ true
1/3 ウサギ true
1/4 コアラ true
餅が食べたい 私も食べたい False
1/4 ゴリラ true
0

3Answer

ループで回してもメモリが32GB程度あるPCなら数千万行くらいまでは大丈夫なのですが、いくつか注意点があります!

  • Pandasのデータフレームのatやiloc、locなどでの特定の行へのアクセスはとても遅いです!
  • そのため、applyなどのメソッドを使うか、一旦リストに直してループを回すと速度的に問題なく処理ができます!

参考 :

※2番目の記事で丁寧に比較してくださった方の情報によると、リストに直す方が速めです!

試しにJupyterで特定位置の行にアクセスする方法でatとリストのインデックスにアクセスする方法を比較してみると、リストの方がはるかに速いとが確認できます(%%timeitという箇所は、マジックコマンドと呼ばれるJupyterの速度計測用の機能を使っています)。

from datetime import date

import pandas as pd

# 仮データです。
df = pd.DataFrame(
    data=[{
        'date': date(2020, 1, 1),
        'animal': 'ネズミ',
    }, {
        'date': date(2020, 1, 3),
        'animal': 'ウサギ',
    }, {
        'date': date(2020, 1, 4),
        'animal': 'コアラ',
    }, {
        'date': '餅が食べたい',
        'animal': '私も食べたい',
    }, {
        'date': date(2020, 1, 1),
        'animal': 'ゴリラ',
    }])
データフレームのatで特定行にアクセスする場合の速度
%%timeit
df.at[2, 'date']
3.07 µs ± 147 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

※1回辺り約3マイクロ秒(1 / 1000 / 1000秒)

# 特定のカラムをリストに一旦変換。
date_list = df['date'].tolist()
リストのインデックスでアクセスする場合の速度
%%timeit
date_list[2]
25.7 ns ± 3.76 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

※1回辺り約25ナノ秒(1 / 1000 / 1000 / 1000秒)

また、isinstance関数も1回辺りナノ秒の世界なので特にパフォーマンスでネックになったりはしないと思われます!

%%timeit
isinstance(date_val, date)
56.4 ns ± 1.58 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

そのため、リスト変換を挟んで対応すると以下のようになります!

from datetime import date

import pandas as pd

# 仮データです。
df = pd.DataFrame(
    data=[{
        'date': date(2020, 1, 1),
        'animal': 'ネズミ',
    }, {
        'date': date(2020, 1, 3),
        'animal': 'ウサギ',
    }, {
        'date': date(2020, 1, 4),
        'animal': 'コアラ',
    }, {
        'date': '餅が食べたい',
        'animal': '私も食べたい',
    }, {
        'date': date(2020, 1, 1),
        'animal': 'ゴリラ',
    }])

# 日付の行であればTrueを格納するリスト(is_date_bools)を
# 作成・判定しています。
date_list = df['date'].tolist()
is_date_bools = []
for date_val in date_list:
    is_date_bools.append(isinstance(date_val, date))

print('-' * 20)
print('各行の判定結果:', is_date_bools)
print('-' * 20)

# 日付判定がTrueの行のみ残したデータフレームを取得します。
sliced_df = df[is_date_bools]

print('算出結果:\n', sliced_df)
--------------------
各行の判定結果: [True, True, True, False, True]
--------------------
算出結果:
   animal        date
0    ネズミ  2020-01-01
1    ウサギ  2020-01-03
2    コアラ  2020-01-04
4    ゴリラ  2020-01-01

些細な点ですが、真偽値との比較は〇〇 == Trueといったようにせずに、真偽値単体で比較するように、とPEP8で定められているので、df[is_date_bools == True]とせずにdf[is_date_bools]としています!(確か前者だとflake8やらautopep8などのLintに引っかかってしまったかもしれません)

参考 : [Pythonコーディング規約]PEP8を読み解く

試しに100万行くらいのデータで速度を測ってみます!(NumPyを使ってランダムな値を各行で選択してデータフレームを作っています)

import numpy as np

date_choices = (
    date(2020, 1, 1),
    date(2020, 1, 2),
    date(2020, 1, 3),
    '餅が食べたい',
)
animal_choices = (
    'ネズミ',
    'ウサギ',
    'コアラ',
    'ゴリラ',
)

df = pd.DataFrame(
    columns=['date', 'animal'],
    index=np.arange(0, 1000000))
df['date'] = np.random.choice(date_choices, size=len(df))
df['animal'] = np.random.choice(animal_choices, size=len(df))

print('用意された100万行のデータフレーム:\n', df)
用意された100万行のデータフレーム:
               date animal
0       2020-01-03    コアラ
1       2020-01-01    ネズミ
2       2020-01-02    コアラ
3       2020-01-03    ウサギ
4           餅が食べたい    ネズミ
...            ...    ...
999995      餅が食べたい    コアラ
999996  2020-01-03    ネズミ
999997  2020-01-02    ゴリラ
999998  2020-01-01    ネズミ
999999  2020-01-02    ネズミ

[1000000 rows x 2 columns]
100万行で処理時間を測ってみるサンプル
%%timeit
date_list = df['date'].tolist()
is_date_bools = []
for date_val in date_list:
    is_date_bools.append(isinstance(date_val, date))
sliced_df = df[is_date_bools]
192 ms ± 791 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

100万行で私の環境で192ミリ秒(約0.2秒)なので、(びったり線形に計算時間が伸びないところもあるとは思いますが)数千万行程度であれば数秒で終わると思われます!

さらに大きいデータ、例えば億行の世界でメモリや計算時間が厳しい・・・みたいなケースでは、Pandasのように扱えて大規模データで計算ができるDaskやVaexなどのライブラリの利用をご検討ください:grinning:

参考 : PythonのDaskをしっかり調べてみた(大きなデータセットを快適に扱う)

※参考になりましたらLGTMぽちっと押していただけますと喜びます・・・!

2Like

Comments

  1. @wellwell3176

    Questioner

    @simonritchie
    多岐に渡る回答および解説、誠にありがとうございます。大変参考になりました。
    リスト化した上での処理についてこれから勉強させていただきます。

@simonritchie さんの回答からデータフレーム定義部分を借用いたしまして、

import numpy as np
from datetime import date
import pandas as pd

# 仮データです。
df = pd.DataFrame(
    data=[{
        'date': date(2020, 1, 1),
        'animal': 'ネズミ',
    }, {
        'date': date(2020, 1, 3),
        'animal': 'ウサギ',
    }, {
        'date': date(2020, 1, 4),
        'animal': 'コアラ',
    }, {
        'date': '餅が食べたい',
        'animal': '私も食べたい',
    }, {
        'date': date(2020, 1, 1),
        'animal': 'ゴリラ',
    }])

isinstance = np.vectorize(isinstance)
df2 = df[isinstance(df['date'].values, date)]
2Like

Comments

  1. nunpy.vectorizeについて調べ直してみたところ、
    numpy.vectorize(関数)
    より
    numpy.frompyfunc(関数, 引数の数, 返り値の数)
    の方がパフォーマンスが良いようです。
  2. @wellwell3176

    Questioner

    @Cartelet 様
    このような方法が・・・。
    dataframeはisinstanceに入らないので諦めていたのですが、
    ベクトル化すれば一括でisinstanceの引数に使えるんですね。
    大変参考になりました。

Your answer might help someone💌