Pandas 集中講座 その3 欠陥データと操作

Pandasライブラリを覚えたい!公式「10 Minutes to pandas」をモチーフに使って実際に動作させ学習する。第3回 欠陥データと操作

Pandas 集中講座(3)

Pandas 集中講座 その1 - chiyoh’s blog
Pandas 集中講座 その2 - chiyoh’s blog
Pandas 集中講座 その3 - chiyoh’s blog
Pandas 集中講座 その4 - chiyoh’s blog
Pandas 集中講座 その5 - chiyoh’s blog
Pandas 集中講座 その6 - chiyoh’s blog

Pandasとは、なにか?

 初め考えていたのは、EXCELシートも扱えるPythonスプレッドシートライブラリ。 VBAの代わりにマクロをPython上で実行できる便利なものと考えていた。

Pandasを実際に使ってみると

 始めて使ったのは、テキストデータをEXCELのxlsxに変換しておきたかったから。実際に変換してみると思ったより難しい(実際には、我々がEXCEL表計算としてとらえていなくて計算機能と印刷機能を持つワープロとして使っているのが原因)。また、Pandasも高機能なので出来ることが多く大いに迷ったが、それでもEXCELファイルの書き出しに成功したのでこれは便利だ!ということでこれを機に少し使ってみようと考えた。

公式「10 Minutes to pandas」を実際にやってみる

 Pandasを覚える!さて、グーグル先生に教えてもらえば大抵のことは分かるのだが、それが今の最適解なのか?と考えるとそうではない。グーグル先生のサーチ機能は、カンニングペーパーであり、単語帳である。欲しい答えを最小ステップでたどり着くための方法。例えるなら、望遠鏡、又は顕微鏡で拡大されたエリアで見つけた希望(答え)。なぜそうなるのか?なぜそう書けば動くのか?など、答えに対する成り立ちがすっぽり抜けてしまうのである。最近のPython環境は、Jupyter Notebookで敷地も下がって来ている。入門書や学習本を眺めて図を見てなんとなくへーとか言って分かった気になって何も残らないパターンになりかねない。今回選んだ方法は、グーグル先生の支援のもと公式のチュートリアル以前のPandas概要説明『10 Minutes to pandas』を上から順にJupyter Notebookを使い実行していってみようと思う。これにより、学習講座のWorkshop形式で講師が腹痛で欠席したので自己学習してなさい版くらいの知識に残ることを期待。実行した結果をここに残し、その時のメモ書きとエッセンスと晒してみる。

pandas.pydata.org

【解説】公式「10 Minutes to pandas」をJupyter Notebookを使って動作させながら確認し、突っ込みを入れる!(この文章を見る価値としては、原書が何をやっているのかが分かる、、、分かるはずである)

公式「10 Minutes to pandas」を実際にやってみる (3/6)

4.欠落データ

Pandasは主にnp.nan欠損値を表すために値を使用します。デフォルトでは計算に含まれていません。欠損データセクションを参照してください。

インデックスを再作成すると、指定した軸のインデックスを変更/追加/削除できます。これはデータのコピーを返します。

df
A B C D F G
2019-05-01 0.000000 0.000000 -0.871839 5 1 1.0
2019-05-02 -0.092376 0.036220 -0.146521 5 2 2.0
2019-05-03 -0.307696 -0.051588 -0.413379 5 3 3.0
2019-05-04 0.397105 1.731472 -1.033554 5 4 NaN
2019-05-05 1.376085 0.310974 0.350129 5 5 NaN
2019-05-06 0.821727 0.389651 -1.202223 5 6 NaN

reindexを使って作り直す

df1 = df.reindex(index=dates[0:4], columns=list(df.columns) + ['E'])
df1
A B C D F G E
2019-05-01 0.000000 0.000000 -0.871839 5 1 1.0 NaN
2019-05-02 -0.092376 0.036220 -0.146521 5 2 2.0 NaN
2019-05-03 -0.307696 -0.051588 -0.413379 5 3 3.0 NaN
2019-05-04 0.397105 1.731472 -1.033554 5 4 NaN NaN

【解説】reindexを使ってindexとcolumnをリビルドしている。E列は、dataが指定されてないのでいわゆる未定義とかNULLの状態

df1.loc[dates[0]:dates[1], 'E'] = 1
df1
A B C D F G E
2019-05-01 0.000000 0.000000 -0.871839 5 1 1.0 1.0
2019-05-02 -0.092376 0.036220 -0.146521 5 2 2.0 1.0
2019-05-03 -0.307696 -0.051588 -0.413379 5 3 3.0 NaN
2019-05-04 0.397105 1.731472 -1.033554 5 4 NaN NaN

【解説】ラベル(.loc)を使って行選択し'E'列に1を代入

データが欠落している行を削除します。

df1.dropna(how='any')
A B C D F G E
2019-05-01 0.000000 0.00000 -0.871839 5 1 1.0 1.0
2019-05-02 -0.092376 0.03622 -0.146521 5 2 2.0 1.0

【解説】dropnaを使ってN/Aを落としている。df1全体からNaNのデータを以外を選択

【余談】判断結果としてのNaN以外に、取り込んだデータがNaNである理由は、測定ミス、測定忘れ、記録忘れ、データが飛び値になっていて明らかに記録読み取りミスなどいろいろ考えられるがこういう値に対して適当に自分に都合がよい値を入れてしまうことを改竄といいます。学校の先生方は、よくテスト問題でこれを実行してテスト問題を作ってます。あれは、都合の良い答えを作ってそれをもとに問題を作成していく手法なのかもしれませんが(いやいやあれは、合わせこみっていうものだよ!)。話がそれました

欠落データを埋める。

df1.fillna(value=5)
A B C D F G E
2019-05-01 0.000000 0.000000 -0.871839 5 1 1.0 1.0
2019-05-02 -0.092376 0.036220 -0.146521 5 2 2.0 1.0
2019-05-03 -0.307696 -0.051588 -0.413379 5 3 3.0 5.0
2019-05-04 0.397105 1.731472 -1.033554 5 4 5.0 5.0

【解説】df1全体からNaNを探しvalue値を代入する。

【余談】Seriesや、DataFrameにするデータのNaNの扱いは注意しましょう。Pandasは、NaNの扱いが上手みたいなのでいろいろできます。NaNの値をどういう理由で変更するのかソースコードコメントとして残していくのがベターと思われます。後で、データの改竄といわれないように

NaN値が格納されているブールマスクを取得します

pd.isna(df1)
A B C D F G E
2019-05-01 False False False False False False False
2019-05-02 False False False False False False False
2019-05-03 False False False False False False True
2019-05-04 False False False False False True True

【解説】isnaを使い。Is N/A? ってことなんでしょう。"Not Applicable" "Not Available" え?"Not a Number"とちがうじゃん!と思うのですが適応範囲外の値ということでは同じなんでしょうね。True/False(真/偽)(Yes/No)(合否)、、、Trueの部分がNaNが入っている箇所です
【余談】isna他でもう説明したような

pd.notna(df1)
A B C D F G E
2019-05-01 True True True True True True True
2019-05-02 True True True True True True True
2019-05-03 True True True True True True False
2019-05-04 True True True True True False False

【解説】ちなみにnotnaは「NaNじゃない」です。

5.操作

バイナリ演算の基本セクションを参照してください。

統計

一般的な操作は、欠けているデータを除外します。

記述統計を実行します

df
A B C D F G
2019-05-01 0.000000 0.000000 -0.871839 5 1 1.0
2019-05-02 -0.092376 0.036220 -0.146521 5 2 2.0
2019-05-03 -0.307696 -0.051588 -0.413379 5 3 3.0
2019-05-04 0.397105 1.731472 -1.033554 5 4 NaN
2019-05-05 1.376085 0.310974 0.350129 5 5 NaN
2019-05-06 0.821727 0.389651 -1.202223 5 6 NaN
df.mean()
A    0.365808
B    0.402788
C   -0.552898
D    5.000000
F    3.500000
G    2.000000
dtype: float64

【解説】mean() 平均です。列毎に集計します。G列の平均が2になってます。(1+2+3)/6で1ではなく、(1+2+3)/3で2になってます。素晴らしい

他の軸でも同じ操作です。

df.mean(1)
2019-05-01    1.021360
2019-05-02    1.466221
2019-05-03    1.704556
2019-05-04    2.019005
2019-05-05    2.407438
2019-05-06    2.201831
Freq: D, dtype: float64

【解説】df.mean()では列毎でしたが、df.mean(1)では、行(index)毎になります。

df.mean(0)
A    0.365808
B    0.402788
C   -0.552898
D    5.000000
F    3.500000
G    2.000000
dtype: float64

【解説】何が軸なの?に対してdf.mean(0)だと列毎になります。つまりNumpy風axis=0,1という意味ですね。

次元が異なり、位置合わせが必要なオブジェクトを操作します。さらに、Pandasは指定された次元に沿って自動的にブロードキャストします。

s = pd.Series([1, 3, 5, np.nan, 6, 8], index=dates).shift(2)
s
2019-05-01    NaN
2019-05-02    NaN
2019-05-03    1.0
2019-05-04    3.0
2019-05-05    5.0
2019-05-06    NaN
Freq: D, dtype: float64

【解説】shift(2)で下側に2つシフトしています。上2つは定義無いのでNaNです

s = pd.Series([1, 3, 5, np.nan, 6, 8], index=dates)
s
2019-05-01    1.0
2019-05-02    3.0
2019-05-03    5.0
2019-05-04    NaN
2019-05-05    6.0
2019-05-06    8.0
Freq: D, dtype: float64
s=s.shift(2)
s
2019-05-01    NaN
2019-05-02    NaN
2019-05-03    1.0
2019-05-04    3.0
2019-05-05    5.0
2019-05-06    NaN
Freq: D, dtype: float64

【解説】ですです。shiftしてもindex値は変更しないことに注意してください

s=s.shift(10)
s
2019-05-01   NaN
2019-05-02   NaN
2019-05-03   NaN
2019-05-04   NaN
2019-05-05   NaN
2019-05-06   NaN
Freq: D, dtype: float64

【余談】あうあう

s = pd.Series([1, 3, 5, np.nan, 6, 8], index=dates).shift(-2)
s
2019-05-01    5.0
2019-05-02    NaN
2019-05-03    6.0
2019-05-04    8.0
2019-05-05    NaN
2019-05-06    NaN
Freq: D, dtype: float64

【解説】-2で上側に2つシフト

s.shift(1)
2019-05-01    NaN
2019-05-02    5.0
2019-05-03    NaN
2019-05-04    6.0
2019-05-05    8.0
2019-05-06    NaN
Freq: D, dtype: float64

【解説】シフトしたデータは、消えているので戻りません。

df.sub(s, axis='index')
A B C D F G
2019-05-01 -5.000000 -5.000000 -5.871839 0.0 -4.0 -4.0
2019-05-02 NaN NaN NaN NaN NaN NaN
2019-05-03 -6.307696 -6.051588 -6.413379 -1.0 -3.0 -3.0
2019-05-04 -7.602895 -6.268528 -9.033554 -3.0 -4.0 NaN
2019-05-05 NaN NaN NaN NaN NaN NaN
2019-05-06 NaN NaN NaN NaN NaN NaN

【解説】df.subって、一瞬Main/Subとかで、範囲取り出しの処理かと思ったら。subtraction(引き算)のことじゃん!sのSeriesとdfのindexをマッチングさせて引き算している。ってsubtractがあるが・・・

df.subtract(s, axis='index')
A B C D F G
2019-05-01 -5.000000 -5.000000 -5.871839 0.0 -4.0 -4.0
2019-05-02 NaN NaN NaN NaN NaN NaN
2019-05-03 -6.307696 -6.051588 -6.413379 -1.0 -3.0 -3.0
2019-05-04 -7.602895 -6.268528 -9.033554 -3.0 -4.0 NaN
2019-05-05 NaN NaN NaN NaN NaN NaN
2019-05-06 NaN NaN NaN NaN NaN NaN

適用する

import pandas as pd
import numpy as np
dates = pd.date_range('20130101', periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
df
A B C D
2013-01-01 -1.174480 0.131666 0.615321 -0.674131
2013-01-02 0.360878 0.286958 -0.901880 -1.644234
2013-01-03 0.175645 0.967822 -0.817930 -0.160181
2013-01-04 -2.190213 -0.217229 0.158892 0.891357
2013-01-05 0.408453 0.685262 2.027146 -0.426742
2013-01-06 0.909910 -0.743968 0.980539 -1.879472

データに関数を適用する:

df.apply(np.cumsum)
A B C D
2013-01-01 -1.174480 0.131666 0.615321 -0.674131
2013-01-02 -0.813602 0.418624 -0.286558 -2.318364
2013-01-03 -0.637957 1.386446 -1.104489 -2.478546
2013-01-04 -2.828170 1.169217 -0.945597 -1.587188
2013-01-05 -2.419717 1.854479 1.081549 -2.013930
2013-01-06 -1.509807 1.110511 2.062088 -3.893402

【解説】np.cumsumは、累積加算する関数。上から順に足していった値になる。

np.cumsum([1,2,3,4,5,6,7,8,9,10])
array([ 1,  3,  6, 10, 15, 21, 28, 36, 45, 55], dtype=int32)
np.sum([1,2,3,4,5,6,7,8,9,10])
55

【解説】np.cumsumは、使ったことなかったので、リストを入れた結果を表示してみた。予想通り。つまり、列毎にcumsumが適応されて実行されたということになる。オブジェクト指向なのでcumsumcumsum()は違うもの、名詞と動詞みたいな感じか。前者がメソッドの自身を示し、後者が実行する関数を示している感じ。cumsumを使って適応して!っていうのがdf.apply(np.cumsum)の意味合い。

np.cumsum(df)
A B C D
2013-01-01 -1.174480 0.131666 0.615321 -0.674131
2013-01-02 -0.813602 0.418624 -0.286558 -2.318364
2013-01-03 -0.637957 1.386446 -1.104489 -2.478546
2013-01-04 -2.828170 1.169217 -0.945597 -1.587188
2013-01-05 -2.419717 1.854479 1.081549 -2.013930
2013-01-06 -1.509807 1.110511 2.062088 -3.893402
type(np.cumsum(df))
pandas.core.frame.DataFrame

【解説】であれば、本来の使い方np.cumsum(df)としたら?あ、できた。dfをnp.cumsum()が扱える値にcastして実行しているはず。が、戻り結果はpandas.core.frame.DataFrameのまま?なんでしょうね。しかし、微妙に結果が変わっていて。演算はfloat扱いになってますね。

【コメント】お!出たなlambdaだ。謎な引数をもとに演算して結果を謎な出力に戻すということをしているやつですね。 df.applyをちょっと調べると。axis = 0が、デフォなので列毎にSeriesにを切り出して処理をしている。df.applyは、Series切り出しオブジェクトを入力として与え、結果を受け取るということか。lambdaを何回も回るのではなく1回適応で終わり。

df.apply(lambda x: x.max() - x.min())
A    3.100123
B    1.711790
C    2.929026
D    2.770829
dtype: float64
df['A'].max() - df['A'].min()
3.100122888374953

【解説】上記の結果をcolumn毎に実行した結果になったと

ヒストグラム

Histogramming and Discreizationsで詳細を参照してください。

s = pd.Series(np.random.randint(0, 7, size=10))
s
0    0
1    3
2    2
3    1
4    4
5    4
6    2
7    6
8    6
9    1
dtype: int32
s.value_counts()
6    2
4    2
2    2
1    2
3    1
0    1
dtype: int64
type(s.value_counts())
pandas.core.series.Series

【コメント】おい!終わりかよ。きっと苦手な分野なのかな?

s.value_counts().index
Int64Index([6, 4, 2, 1, 3, 0], dtype='int64')

【解説】ヒストグラム?頻度を数えている。上記のSeriesに3が3回出現し、6が2回、、、という結果を違うSeriesに戻してる。作られているindexはintになっている

s = pd.Series(np.random.randn(1000))
s.value_counts(bins=10,sort=False)
(-3.017, -2.416]       6
(-2.416, -1.822]      31
(-1.822, -1.228]      68
(-1.228, -0.634]     158
(-0.634, -0.0396]    237
(-0.0396, 0.555]     247
(0.555, 1.149]       154
(1.149, 1.743]        64
(1.743, 2.337]        28
(2.337, 2.931]         7
dtype: int64

【解説】Numpyで1000個乱数を作成し10区画に分けたヒストグラムを作成。瓶の範囲が明確に表示されているのがいい感じですね。また、s.value_countsの目的がSeriesで同じ値になるものを多い順に並べなさいという関数ですね。indexがそのキーでdataが頻度になる。出てくる結果が多いbest5を調べるときとか便利そうですね

文字列メソッド

以下のコードスニペットのように、seriesはstr属性に一連の文字列処理メソッドを備えているため、配列の各要素に対する操作が簡単になります。strのパターンマッチングは一般にデフォルトで正規表現を使います(そして場合によっては常にそれらを使います)。詳しくはベクトル化された文字列メソッドをご覧ください。

【解説】コードスニペット(code snippet)ソースコードの断片

s = pd.Series(['A', 'B', 'C', 'Aaba', 'Baca', np.nan, 'CABA', 'dog', 'cat'])
s
0       A
1       B
2       C
3    Aaba
4    Baca
5     NaN
6    CABA
7     dog
8     cat
dtype: object
s.str.lower()
0       a
1       b
2       c
3    aaba
4    baca
5     NaN
6    caba
7     dog
8     cat
dtype: object

【解説】Seriesのdata形式が文字列の場合いろいろと便利な関数が用意されているよ。ということですね。上記の例は、各データを小文字化するlower()という関数です。注意点としては、NumpyとPandasでは文字をstrとして認識せず、数値以外のものでしょうがなく扱っているオブジェクト型として認識しているのでその辺が注意です。dtypeがstrではなくobjectです

type('ABC')
str
s.str.len()
0    1.0
1    1.0
2    1.0
3    4.0
4    4.0
5    NaN
6    4.0
7    3.0
8    3.0
dtype: float64

【コメント】Excelでもそうですがセルに入っている文字列を処理することを多いですね。
詳しくはベクトル化された文字列メソッドをご覧ください。の部分時間がある時に調べるといろいろとできる幅が広がりそうです。区切り位置とかどうやるんだろ。columnを横断してしまうけどなどと思考をはしらせてしまいますね。

chiyoh.hatenablog.com
chiyoh.hatenablog.com