pandasのデータフレームから重複行を削除する

前回の記事が重複行の抽出だったので今回は重複行の削除です。
参考:pandasのデータフレームの重複行を抽出する

このあと専用の関数を紹介しますが、
重複行の抽出条件を反転させればできてしまうので、この書き方でも構わないと思います。
print(df[~df.duplicated()])
(むしろ、duplicated関数の引数keepの意味はこちらの方がわかりやすい。)


import pandas as pd
import numpy as np

# データの作成
df = pd.DataFrame(
        {
            "col0": np.random.choice(["A", "B", "C"], size=10),
            "col1": np.random.choice(["a", "b", "c"], size=10),
            "col2": np.random.choice(["1", "2", "3"], size=10),
        }
    )
# データの確認
print(df)
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
4    B    a    2
5    C    a    1
6    C    b    2
7    B    a    2
8    C    c    3
9    C    c    3
'''
# duplicated()の結果を反転させると重複していない行を抽出できる。
print(df[~df.duplicated()])
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
6    C    b    2
8    C    c    3
'''

pandasには重複行削除専用のメソッドとして、drop_duplicatesが定義されています。
ドキュメントはこちら。
pandas.DataFrame.drop_duplicates

引数の keep と subset は duplicated と同じように使えます。
また、inplace に Trueを指定することで、そのデータフレームの値自体を書き換えます。
デフォルトはFalseで、重複行を削除した結果を返します。


print(df.drop_duplicates())
'''
  col0 col1 col2
0    C    a    1
1    B    a    2
2    A    c    3
3    B    c    1
6    C    b    2
8    C    c    3
'''

先ほどの結果と同じもののが得られましたね。

pandasの実装を見るとわかるのですが、ただのラッパーなので、
とくにこちらの方が実行速度が速いといったことはなさそうです。
https://github.com/pandas-dev/pandas/blob/v0.24.2/pandas/core/frame.py#L4605-L4637

pandasのデータフレームの重複行を抽出する

データフレームに入ったデータについて、重複がないか確認したい場面は結構あると思います。
僕の場合は、そういう時はSQLでデータを抽出する段階でユニークにしてしまうことが多いです。
また、特定の列の値(idなど)の一意性の確認であれば、value_counts()して、2個以上含まれている値が無いか見たりします。
それ以外のケースでも、groupbyやsetなど使ってゴリゴリコードを書いてどうにかすることが多いのですが、
せっかく専用の関数が用意されているので今後はそれを使うようにしたいです。

ということで、今回はduplicated()の紹介です。
ドキュメントはこちら。
pandas.DataFrame.duplicated

pythonを勉強し始めた頃、引数なしで実行して、
出力が直感的でなくて使いにくいなーと思っていたのですが、subsetやkeepなどの引数をきちんと理解すれば非常に便利です。

とりあえず実験するために、ダミーデータを作っておきます。


import pandas as pd
import numpy as np

df = pd.DataFrame(
        {
            "col0": np.random.choice(["A", "B", "C"], size=10),
            "col1": np.random.choice(["a", "b", "c"], size=10),
            "col2": np.random.choice(["1", "2", "3"], size=10),
        }
    )

print(df)
'''
出力 (乱数を使っているので実行するたびに変わります。)
  col0 col1 col2
0    B    b    1
1    C    c    2
2    A    b    1
3    B    a    2
4    A    b    1
5    C    b    3
6    A    b    1
7    B    b    1
8    A    b    3
9    B    b    3
'''

このデータフレームに対して、引数無しで実行したものがこちらです。


print(df.duplicated())
'''
0    False
1    False
2    False
3    False
4     True
5    False
6     True
7     True
8    False
9    False
dtype: bool
'''

これ、一見、インデックス4,5,6の行が重複してるという意味に勘違いしそうなのですがそうではありません。
引数を省略すると、keep="first"を指定したことになり、重複行のうち、最初の値はFalseになります。
なので、インデックス4の行と重複してる行が0~3の間にあるという意味です。(実際はインデックス2)

keep="last"を指定すると、逆に重複している行のうち、
最後の行はFalse、それ以外の行はTrueになります。(重複してない行はFalse)
こちらの方が、時系列に並べたデータのうち、最新のデータを取りたい場面などで重宝すると思います。
また、 subsetに列名の集合を渡すと、その列だけを使って判定を行います。
これもまとめて試してみましょう。


print(df.duplicated(subset=["col1", "col2"], keep="last"))
'''
0     True
1    False
2     True
3    False
4     True
5     True
6     True
7    False
8     True
9    False
dtype: bool
'''

重複した行全部を抽出したい、という場合は、keep=Falseを指定します。
また、先に紹介した2例でもそうなのですが、実際に使う時は重複した行のインデックスではなくて、
その行そのものをみたいという場面が多いので、次のような使い方になると思います。


print(df[df.duplicated(subset=["col1", "col2"], keep=False)])
'''
  col0 col1 col2
0    B    b    1
2    A    b    1
4    A    b    1
5    C    b    3
6    A    b    1
7    B    b    1
8    A    b    3
9    B    b    3
'''

df[]の中に放り込んであげるだけですね。
コードのテスト時など、無いはずの重複が本当に無いことを確認する時などは、
duplicated関数は結構便利です。

重複を除外する時などはまた専用の関数があるので次に紹介します。

pandasで日時を表す文字列を時刻型に変換

前回の記事の予告通り、
pandasのto_datetime関数の紹介です。

pandas.to_datetime

非常に多くのデータ型や、フォーマットに対応していて柔軟に時刻データに変化してくれます。
ドキュメント中に書かれている引数:argの通り、Seriesでも一個の文字列でも大丈夫です。

Parameters:
arg : integer, float, string, datetime, list, tuple, 1-d array, Series
New in version 0.18.1: or DataFrame/dict-like

実際に使ってみましょう。


import pandas as pd
pd.to_datetime("2019/05/17 07:25:34")
# Timestamp('2019-05-17 07:25:34')

pd.to_datetime("2019-05-17 07:25:34.372245")
# Timestamp('2019-05-17 07:25:34.372245')

pd.to_datetime("2019 May 17  07:25:34.372245")
# Timestamp('2019-05-17 07:25:34.372245')

pd.to_datetime("17-05-19 07:25:34")
# Timestamp('2019-05-17 07:25:34')

上の例ではフォーマットを変えながら文字列を渡しましたが、いい感じに解釈してくれているのがわかります。
%Y-%m-%dとか指定しなくていいのでとても楽です。

これがあるので、自分はstrptimeはほぼ使っていません。

文字列を一つ渡せば、Timestampが戻り、Seriesを渡せばSeriesが帰ってきますが、
listを渡した時はlistではなく、DatetimeIndexが返されるのは注意です。

あとは、Timestamp型と、datetime型の違いがきになるところですが、
ほぼ互換性があると書いてあるので恐らく大丈夫でしょう。
(細かな違いもそのうち調べようとは思っていますが。)

参考: pandas.Timestamp

Timestamp is the pandas equivalent of python’s Datetime and is interchangeable with it in most cases. It’s the type used for the entries that make up a DatetimeIndex, and other timeseries oriented data structures in pandas.

pythonで日付を表す文字列をdatetime型に変換する

自分は滅多に使わないのですが、前回の時期で strftime を紹介したので、
その逆の変換を行う strptime も紹介しておきます。

参考:pythonで今日の日付を表す文字列をつくる
前の記事中にも少し書いてますが、strptime を使おうとすることよりも、strftimeと間違えて書いてしまうことが多いです。

ドキュメントはこちら。
lassmethod datetime.strptime(date_string, format)

書いてある通り、日時を表す文字列と、そのフォーマットを渡してあげるとdatetime型に変えてくれます。
使い方のイメージ。


import datetime
dt = datetime.datetime.strptime("2019-08-01 07:31:25", "%Y-%m-%d %H:%M:%S")
dt
#  datetime.datetime(2019, 8, 1, 7, 31, 25)
print(dt)
# 2019-08-01 07:31:25

フォーマットを指定するのが面倒ですね。

%Yや%mなど、使える文字の一覧はこちらにあります。
strftime() と strptime() の振る舞い

個人的な感想としては、あまり使い勝手が良くないので、
次の記事で紹介する、pandasのto_datetime関数を採用することが多いです。
(結果の型がTimestampなので、完全に互換なものではないのですが、実用上これで困らない。)

pythonで今日の日付を表す文字列をつくる

プログラミングの小ネタです。
集計結果をファイルに保存するときに、ファイル名に日付を入れておくことがあります。
コード中にハードコーディングして動かすたびに書き換えてもいいのですが、
普通は自動的に今日の日付が入るようにしておいたほうが楽なのでその方法を紹介します。

これは標準ライブラリの datetime で実現できます。
ドキュメントはこちら
datetime — 基本的な日付型および時間型
datetimeの中にもう一回datetimeが作られているとか、色々文句を言いたくなることもあるのですが、
時間処理に必要なことは大抵やってくれます。

実行美の日付を得るには、
datetime.datetime.today() か、 datetime.datetime.now() で現在時刻を取得して、
strftime() メソッドで、フォーマットを指定して表示するのが丁寧だと思います。


import datetime
today = datetime.datetime.today().strftime("%Y-%m-%d")
print(today)
# 2019-05-15

この方法でなんの問題もないのですが、
strftime() が strptime() と間違えやすかったり、フォーマット指定の文字列が大文字だったか小文字だったかド忘れしたりするので、
面倒な時は .date() メソッドで日付型にして時刻情報を捨て、 str()関数で文字列型にするという雑な方法でやっています。


today = str(datetime.datetime.today().date())
print(today)
# 2019-05-15

.date() を外すと、時刻まで出力されるのですが、空白が入るのでファイル名には使いにくくなります。
ただ、ロギングが目的の場合は時刻まであったほうが便利でしょう。

pythonでアスキー文字の一覧を得る

本来は前回の平仮名やカタカナの一覧を作る記事よりこちらを先に書くべきでした。
参考:pythonでひらがなとカタカナのリストを作成する

ここでは、abcなどのアルファベットや0123といった数値のリストを得る方法を紹介します。
実はこれらは組み込み関数にあらかじめ定義されている定数があるので、
平仮名のように文字コードから作ったりする必要はありません。

string — 一般的な文字列操作

各定数の説明は上のドキュメントに書いてあるので、
ここでは具体的にその内容を表示しておきましょう。
タブや空白、改行などもあり、printすると逆に見えなくなる例もあるので、
jupyter notebook で裸で実行した時に表示される文字列をコメントとしてつけました。


import string

string.ascii_letters
# 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

string.ascii_lowercase
# 'abcdefghijklmnopqrstuvwxyz'

string.ascii_uppercase
# 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

string.digits
# '0123456789'

string.hexdigits
# '0123456789abcdefABCDEF'

string.octdigits
# '01234567'

string.printable
#  '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

string.punctuation
# '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

string.whitespace
# ' \t\n\r\x0b\x0c'

pythonでひらがなとカタカナのリストを作成する

pythonでひらがなやカタカナの一覧を作成するスマートな方法を考えてみたので紹介です。
きっかけは、ある所でcsvファイルにあ、い、う、え、お、…と一行一文字書き並べたファイルを読み込んでいるコードを見かけたことです。

この記事でリストを作成する方法を書きますが、その前に本当にひらがな、カタカナのリストが必要なのかということは考えた方が良いと思います。
例えばある文字列がひらがなのみで成り立っているかどうかとか、カタカナが含まれているかと言った判定を行うのであれば、正規表現を使った方がよいです。
リストを準備して、for文で回して一文字ずつチェックするような効率が悪い処理を実装するべきではありません。
正直な所、このリストが必要になる場面は結構珍しいと思います。

それでもひらがなカタカナのリストが必要だという場合は、
ひらがな、カタカナにそれぞれ連続した文字コードを割り当てられていることを利用して生成するのが効率がいいのではないかと思います。
こちらも参照:pythonで文字と文字コードの相互変換

具体的には次のようなコードで作成できます。
生成結果は結合してprintしました。


hiragana = [chr(i) for i in range(ord("ぁ"), ord("ゖ")+1)]
print("".join(hiragana))
# ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ
katakana = [chr(i) for i in range(ord("ァ"), ord("ヺ")+1)]
print("".join(katakana))
# ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ

“あ”より”ぁ”の方が先であること、 最後に +1 しておかないと末尾の文字が抜けることなどに注意です。
“ゐ”や”ゑ”、”ヸ”、”ヹ”、”ヺ”などはいらないって場合もあると思うので必要に応じて微調整してください。
もしかしたら”゛”(濁点)や”゜”(半濁点)、その他”、”や”。”などの句読点なども必要だという場面もあるかもしれません。

ord(“ぁ”)などとせずに、数字で指定しても結果は同じになりますが、
意味が明確になるので、ord(“ぁ”)の方が良いと思います。

参考


hiragana = [chr(i) for i in range(12353, 12439)]
print("".join(hiragana))
# ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ
katakana = [chr(i) for i in range(12449, 12539)]
print("".join(katakana))
# ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ

pandasで要素のユニークカウント

pandasのDataframeやSeriesの要素を重複を排除してカウントとする関数の紹介です。
nuniqueというのを使います。

pandas.DataFrame.nunique
pandas.Series.nunique

分析の中で、ユニークカウントする機会はよくあると思うのですが、この関数の知名度が低いようで、
少し回りくどい方法を取っている人をよく見かけます。

使い方は簡単です。 試しに、4種類の値からなる要素10個の配列でやってみましょう。


import pandas as pd
data = pd.Series(['c', 'b', 'c', 'a', 'a', 'a', 'c', 'a', 'c', 'd'])
print(data.nunique())  # 4

簡単でした。

参考ですが、この関数を使わない方法も色々あります。


# value_counts を使って、その長さを取る。
print(len(data.value_counts()))  # 4

# ユニークな値のリストを取得してその長さを取る。
print(len(data.unique()))  # 4

# 集合に変換してその要素数を取る
print(len(set(data)))  # 4

nunique の知名度を考えると、これらの書き方の方が適切な場面もあるかもしれません。

for文で実装してるような人も見たことがありますが流石にこういうのは避けた方がいいと思います。


# 悪い例
cnt = 0
tmp_set = set()
for d in data:
    if d not in tmp_set:
        cnt += 1
        tmp_set.add(d)
print(cnt)  # 4

標準化レーベンシュタイン距離は距離関数なのか

以前の記事で、標準化レーベンシュタイン距離(標準化編集距離)というのを紹介し、
自分も使っていたのですが挙動に少し違和感があったので確認しました。

参考:標準化レーベンシュタイン距離

レーベンシュタイン距離はその名の通り、距離関数なのですが、
これを標準化してしまうとどうも距離関数っぽくない動きをしてるように思えたのです。

念の為、距離関数というもの自体の定義をおさらいしておきましょう。

集合$X$に対して、$d:X\times X \rightarrow \mathbb{R}$ が距離関数であるとは、
$x,y,z \in X$に対して次の条件が成り立つ時に言います。
1. $d(x,y) \geq 0$ (非負性)
2. $d(x, y) = 0 \Leftrightarrow x = y$ (同一律)
3. $d(x, y) = d(y, x)$ (対象律)
4. $d(x, z) \leq d(x, y) + d(y, z)$ (三角不等式)

この条件のうち、 1. 2. 3. は特に問題ないのですが、
標準化レーベンシュタイン距離 については、4. の三角不等式がちょっと怪しかったです。
で、反例を探してみたところ簡単に見つかりました。
$ld(*,*)$をレーベンシュタイン距離、$nld(*,*)$を標準化レーベンシュタイン距離とし、
x = ‘ab’, y= ‘aba’, z = ‘ba’ とおきます、
すると、
$ld(x, z) = 2$ なので、$nld(x, z) = 1$ ですが、
$ld(x, y) = ld(y, z) = 1$ なので、$nld(x, y) = nld(y, z) = \frac13$ です。

そのため、 $nld(x, z) > nld(x, y) + nld(y, z) = \frac23$ となり、
三角不等式を満たしません。

標準化レーベンシュタイン距離 は 標準化レーベンシュタイン という名前の距離関数と考えるのは誤りで、
レーベンシュタイン距離 という距離関数を標準化したもの(その結果距離関数ではなくなってしまったもの)と、
考える必要があります。

現状これで激しく困ったということはないのですが、
一部のライブラリにある、自分で作った距離関数を引数に渡せるようなものには、
標準化レーベンシュタイン距離は突っ込まない方が安全そうです。

pandasで指数平滑移動平均

昨日の記事が移動平均だったので、今日は指数平滑移動平均を扱います。
初めて知った日は衝撃だったのですが、pandasには指数平滑移動平均を計算する専用の関数が用意されています。
(pythonを使い始める前はExcel VBAでいちいち実装していたので非常にありがたいです。)

馴染みがない人もいると思いますので軽く紹介しておきます。
元のデータを${x_t}$とし、期間$n$に対して指数平滑移動平均${EWMA_t}$は次のように算出されます。
$$
\begin{align}\alpha &= \frac{2}{1+n}\\
EWMA_0 &= x_0\\
EWMA_t &= (1-\alpha)*EWMA_{t-1} + \alpha * x_t
\end{align}
$$

3番目の式を自分自身に逐次的に代入するとわかるのですが、
$EWMA_t$は、$x_t$から次のように算出されます。
$$
EWMA_t = \alpha\sum_{k=0}^{\infty}(1-\alpha)^k x_{t-k}
$$
$(1-\alpha)$の絶対値は1より小さいので、この無限級数の後ろの方の項は無視できるほど小さくなります。
結果的に、過程${x_t}$の最近の値に重みを置いた加重平均と見做せます。

さて、早速ですが計算してみましょう。

pandasのDataFrameおよび、Seriesに定義されているewm関数を使います。
pandas.DataFrame.ewm


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# データ作成
data = pd.Series(np.random.normal(0, 100, 200).cumsum() + 20000)
# 指数平滑移動平均の計算
data_ewm = data.ewm(span=10).mean()
# 可視化
plt.rcParams["font.size"] = 14
fig = plt.figure(figsize=(12, 7))
ax = fig.add_subplot(1, 1, 1)
ax.plot(data, label="元データ")
ax.plot(data_ewm, label="指数平滑移動平均")
plt.legend()
plt.show()

出力がこちら。

ここで一つ注意する点があります。
data_ewm = data.ewm(span=10).mean()
という風に、spanという変数名で期間$10$を渡しています。
ドキュメントを読んでいただくとわかるのですが、span=をつけないと、
comという別の変数に値が渡され、$\alpha$の計算が、
$\alpha=1/(1+com)$となり、結果が変わります。

また、spanやcomを使う以外にも、alpha=で$\alpha$のあたいを直接指定することも可能です。