📝

pandasの威力を手っ取り早く実感したい

2023/08/29に公開

https://qiita.com/items/51b5a8bf9ef41ed04ac2


お詫び

Qiitaの元記事にて、区切り線を「---」で書いている場所があり、これがZennの記法に干渉して一部うまく表示できない記事がある事を認識しています。
全ての記事を精査しきれていないため、お手数ですがお見かけの際は教えていただけると大変喜びます。


私自身、pythonもあまり理解していると言えないレベルだが、そんなレベルでも手っ取り早くパンダさんすげー!っていいたい記事が見つからなかったので書いた。
正直、他の記事とかを読むと、周りのレベルが高すぎて参入障壁を非常に高く感じていたpython,pandasだが、私みたいなおっさん勢でもpythonのなんかすごいライブラリ使い始められるよ!っていうのを伝えたかった。
少しでも難しい話はここではしたくないと思って書いているので、有識者各位から見れば疑問に感じる部分は多くあることをご留意いただきたい。

今回つかったもの
import pandas as pd #pip, xlrd, openpyxl
import sqlite3 #apt install / _sqlite3が無いと言われた場合はpython自体の再インストールを
from pandasql import sqldf #pip

STEP2では以下が必要になる。
import socket

STEP0. 代入をatで軽快にやろう

まえがき

dataframeを使い始めて思ったのが

  • よく分からん
  • 使いやす……い……?
  • dictで良いんじゃないの?

という、pandasとはなんぞや?をまるで理解していない意見だった。
もちろん、pandasの使い方を知らないけどとりあえず使ってみよう、から始まった話なのでこういうトンチンカンな事をやりだすのもよくある話(スマートフォンを知らないけどガラケーから機種変した時のような、あの気分)だが、分からないなりにやってみたが、

遅い

もちろん、使い方を間違えているからに他ならない事は明記しておくが、とりあえずは非効率的なforやなんちゃってリスト内包表記を使うのをやめる事から考えてみた。
その軌跡をたどろうと思う。

本題

import pandas
df = pandas.read_csv('site.csv')  #たったこれだけでCSVが使えるようになるパンダさんすごい
#site.csvにはsite,url,ip,host(サイト名、URL、IP、portなど)が入っているものを想定


#良い例
def good(df):
  for index,row in df.itterrows():
    df.at[index, 'name'] = 'test'

# rowの中にcsvヘッダを持っており、df.(head)[index]で簡単かつ高速に参照できる

#よく分からない時にやっていた事
def poor(df):
for index in range(len(df)):
  df.name[index] = 'test'

# range(len(df))を変数にしたり、リスト内包表記やmapにしても上記例より遅かった

これは手元のコードで比べてもらうと一目瞭然かと思う。
dataframeに代入する処理をgoodのように書き換えるだけでだいぶ変わる。大事なのは(dataframe).at
そこだけ変えるだけでも体感で分かるレベルで早くなった。

~~準備略~~

for df1_index,df1_row in df1.ix[df1.name.str.contains('.*test.*', na=False)].iterrows():
  #nameにtestを含む項目を抜き出す。単純にtestが欲しい場合、df1.ix[df1.name == 'test'].iterrows()
  df1.at[df1_index, 'find_test'] = 'OK'

変わったのはループの範囲。
もっと良い方法があるが、まえがきにも書いた通り私がそこから沼にハマり始めたので、ここでは敢えて割愛する。

STEP1. ムリせずSQLを使う

先述で割愛した通り、pandasには使いやすいメソッドがいっぱいある。
が、逆に多すぎて全部を覚えるのが大変で、とりあえずコピペで作ったコードでは何がなんだか分からなくなる。そのうちメンテやリファクタリングを諦める。

勉強したての頃やメンテ初期は、あまり頑張りすぎずSQLを使った方が良いかも知れない。
とはいえ、本当はこんな事をしなくても良い事も多い。
最終的にはSQLを書かなくていい事はメソッドを使うべきだと思うが、慣れないうちはpandas自体に苦手意識を持たないよう、SQLdfを使うのも良いと思う。

~~上記df.at[index, 'name'] = 'test'を以下の呼び出しなどに読み替える~~

def sql(df1):
  #sql_resultに欲しい結果のSQLを書く。SQLのfrom句にdf1(python側に書いたpandasデータフレーム)
  sql_result = """
    select *
    from df1
    ;:"""
  df2 = sqldf(sql_result,locals())
  #del df2[(たとえば、サブクエリ等WHERE句の比較で取得した列など、表示に要らない列)]
  return df2

df2 = sql(df1)
#df2 = pd.concat([df1, df2,...], ignore_index=True)  #必要であれば列を追加するなり。ignore_index=Trueでdfx..の結果が0件の場合でもエラーとしない

ここでSQLについては触れないが、これで期待した結果と速度を得られるようになる。

STEP2. CSVに項目が存在しないケースに対応する

上記例でURL,ipがあるので、ip <- -> host変換をしてみたいケースを考える。
pandasの便利さが何となく分かってきたところで、ハマりやすいエラーを考える。
そもそも、そういうCSVを送ってくるな!と運用で対処できるのが理想だが、現実的にはそうなってない事が多いので、こういうケースを考えておく。

def ip_host(df, index):
  # 各項目に値がない場合の対処。import numpy として、numpy.NaNで比較しても良いが、以下でも同様の結果が得られる
  if not df.URL == df.URL:
    socket.gethostbyaddr(df2.ip[index])[0]
  elif not df.ip == df.ip:
    socket.gethostbyname_ex(df2.URL[index])[2][0]
  else:
    # URLもipも存在するケース
    pass

もう一つ嫌らしいケースとしては、そもそもCSV自体に項目ipやURLなどが存在しないケースがあった場合は上記では対処しきれない。
たとえばdf.URLやdf.ipが参照できずエラーになるので、try-exceptでエラーハンドリングする必要がある。

STEP3. 脱:SQL

ここから先は本記事の対象を離れて、pandasのメソッドを頑張って探していく事になる。
SQLに頼らなくても書けると非常にスマートなコードになるので、もっとパンダさんと仲良くなれると思う。

私のようにpythonに苦手意識を持っていたり、pandasにとっつきにくさを感じていた人が「何となくやってみようかな」と思ってくれれば幸いだ。

参考

step0の書き方をやめようと思って、大いに参考にさせていただいた方々。
今回登場していない内容もあるが、知っているのと知らないのとでは理解度が違うと思うので挙げさせていただいている。

pandas データフレームをstr.contains()で処理し、nan値を無視したい
https://teratail.com/questions/108500

<Python, pandas> concatで連結した時にindexを再度振り直す。
http://nekoyukimmm.hatenablog.com/entry/2015/10/20/161110

pandasで任意の位置の値を取得・変更するat, iat, loc, iloc
https://note.nkmk.me/python-pandas-at-iat-loc-iloc/

高度な副問合せの構文(SQL)
http://www.atmarkit.co.jp/ait/articles/1209/14/news146.html

Pandas のデータフレームの特定の行・列を削除する
https://pythondatascience.plavox.info/pandas/行・列を削除

PythonではNoneの比較は==ではなくisを使う
http://monmon.hateblo.jp/entry/20110214/1297710749

GitHubで編集を提案

Discussion