芋の独り言

当ブログへのアクセスは当ブログのプライバシーポリシーに同意したものとみなします.

pandas 私的メモ

簡単なpandasデータの作り方

>>> import pandas as pd
>>> twod_array = [[0,1,2],[3,4,5],[6,7,8]]
>>> data =pd.DataFrame(twod_array)
>>> data
   0  1  2
0  0  1  2
1  3  4  5
2  6  7  8
>>> data[:]
   0  1  2
0  0  1  2
1  3  4  5
2  6  7  8
>>> data[:][:]
   0  1  2
0  0  1  2
1  3  4  5
2  6  7  8

:で全要素を選択する

>>> data[3][:]
Traceback (most recent call last):
~~~
KeyError: 3

存在しない行(名)や列(名)の指定でKeyErrorが起きる.

データフレーム

  • pd.Series :1次元配列に対応した表形式データのオブジェクト
  • pd.DataFrame:2次元配列に対応した表形式データのオブジェクト←pd.Seriesの上位互換

基本的に使い方や使えるメソッドは同じ.ただし,扱える次元数に注意する.
value_counts()はpd.Seriesで使えるメソッド.pd.DataFramにはないので,pd.DataFramをスプライシングしてpd.Seriesにしたら使える.

スライシングの方法

>>> data[0]
0    0
1    3
2    6
Name: 0, dtype: int64
>>> data[0][1]
3
>>> data[0:2+1:2]
   0  1  2
0  0  1  2
2  6  7  8
>>> data[0:2+1:2][:]
   0  1  2
0  0  1  2
2  6  7  8
>>> data[:][0:2+1:2]
   0  1  2
0  0  1  2
2  6  7  8
>>> data[0][0:2+1:2]
0    0
2    6
Name: 0, dtype: int64

まとめ

# 列のスライス
data[start:end+1:stime][:]
# 行のスライス
data[:][start:end+1:stime]
  • start :開始
  • end :終了
  • stime:区切り間隔

いくつかのデータの統合

>>> no4ch = pd.DataFrame([0,1,2,3])
>>> no4ch
   0
0  0
1  1
2  2
3  3
>>> no6ch = pd.DataFrame([1,2,3,4])
>>> no6ch
   0
0  1
1  2
2  3
3  4
>>> con=pd.concat([no4ch, no6ch], axis=0)
>>> con
   0
0  0
1  1
2  2
3  3
0  1
1  2
2  3
3  4
>>> con=pd.concat([no4ch, no6ch], axis=1)
>>> con
   0  0
0  0  1
1  1  2
2  2  3
3  3  4
>>> type(con)
<class 'pandas.core.frame.DataFrame'>

pd.concatの第一引数につなげたいデータをリストで渡す.また,

  • axis=0:縦に並べてつなげる
  • axis=1:横に並べてつなげる

となる.行数が一致していない場合は,

FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.

To accept the future behavior, pass 'sort=False'.

To retain the current behavior and silence the warning, pass 'sort=True'.

と警告が出るが,互いにない要素をNaNで埋めて結合してくれているので,特に問題はないかも.
また,con.fillna(0,inplace=True)とすると,NaNを全て0に置き換えるといった処理ができる.

csv出力

以前自身がやってたやり方

# 書き込むファイルのパスが”path”に代入されているという前提のもとのスクリプト
import csv

data = list(data)

with open(path,mode='w') as f:
    writer=csv.writer(f, lineterminator='\n') # writerオブジェクトを作成
    try:
        writer.writerow(data) # 内容を書き込む
    except UnicodeEncodeError as ue:
        message(ue,'csvファイルの書き込みに失敗しました')

pandasでのやり方

# 書き込むファイルのパスが”path”に代入されているという前提のもとのスクリプト
data.to_csv(path)

その他引数を指定できる.

列数が一定でないcsvの読み込み

列(カラム)数が一定でないcsvファイルをpandasで読み込もうとすると,pandas.errors.ParserErrorというエラーが出る.なので,

# 読み込むファイルのパスが”csv”に代入されているという前提のもとのスクリプト

def csv_column_name(csv):
    column_cnt = 0
    with open(csv,mode='r',encoding='utf-8') as f:
        for line in f:
            cnt = len(line.split(','))
            if column_cnt < cnt:
                column_cnt = cnt
    return [x for x in range(column_cnt)]

data = pd.read_csv(csv, engine='python',encoding='utf-8',header=None,names=csv_column_name(csv))

として,カラム名を作成して指定するとよい.

参考

追加でメモ(2020/9/2)

参考リストは上記の参考に追加してあります.

Dataframeのsum()はSeriesなのでfor文でそのまま回せる

>>> a=pd.DataFrame({"a":{"c":1},"b":{"d":2}})
>>> a
     a    b
c  1.0  NaN
d  NaN  2.0
>>> a.fillna(0)
     a    b
c  1.0  0.0
d  0.0  2.0
>>> a
     a    b
c  1.0  NaN
d  NaN  2.0
>>> a.sum()
a    1.0
b    2.0
dtype: float64
>>> type(a.sum())
<class 'pandas.core.series.Series'>
>>> a.sum(axis=1)
c    1.0
d    2.0
dtype: float64
>>> type(a.sum(axis=1))
<class 'pandas.core.series.Series'>
>>> for i in a.sum():
...     print(i)
...     
1.0
2.0
>>> a["a"]
c    1.0
d    NaN
Name: a, dtype: float64
>>> a["a"] = a["a"]/a.sum()["a"]
>>> a
     a    b
c  1.0  NaN
d  NaN  2.0
>>> a["a"] = a["a"]*2
>>> a
     a    b
c  2.0  NaN
d  NaN  2.0
>>> a = a.fillna(1)
>>> a["a"] = a["a"]*2
>>> a
     a    b
c  4.0  1.0
d  2.0  2.0

fillna(指定の数もしくは文字列)でNaNをすべて引数で置き換える. また,method="ffill"でNaNの前の数値で置き換えということができるみたい.

sum()で各列もしくはaxis=1で各行ごとの合計がSeriesとして得られるため, Dataframeの場合for column_name, item in df.iteritems():せずにfor文でそのまま回して値だけを得ることができる.

また,Dataframeの列を数値で四則演算すると,各要素ごとにその演算が適用される. 上記の場合0を割ってしまっている部分があり,そこはNaN値になっている.

>>> for i in a.sum().index:
...     print(i,a.sum()[i])
...     
a 6.0
b 3.0
>>> for k,v in enumerate(a.sum()):
...     print(k,v)
...     
0 6.0
1 3.0
>>> a.sum()[0]
6.0

Seriesは.indexでインデックスが得られるので,sum()を呼び出すのが2度手間になってるが, インデックスを得つつ値も同時に処理できるようになる. リストなどでお馴染みのenumerate()を使うとリストのようにインデックス番号と値が得られるが, インデックスが文字列の場合は適さないかもしれない. インデックス番号でもSeriesから要素の値を得られるのでどっちでも良いのかもしれないが...

要素へのアクセス

スライスや要素のアクセスについてちょっと分からなくなった... この記事内にスライスについて書いてはあるが,改めて復習させてください~

>>> a
     a    b
c  4.0  1.0
d  2.0  2.0
>>> a["a"]
c    4.0
d    2.0
Name: a, dtype: float64
>>> a["b"]
c    1.0
d    2.0
Name: b, dtype: float64
>>> a[:]["a"]
c    4.0
d    2.0
Name: a, dtype: float64
>>> a["c"]
Traceback (most recent call last):KeyError: 'c'
>>> a[:"c"]
     a    b
c  4.0  1.0
>>> a[0:1]
     a    b
c  4.0  1.0
>>> a[:"c"]["a"]
c    4.0
Name: a, dtype: float64
>>> a["a"][:"c"]
c    4.0
Name: a, dtype: float64
>>> a["c":"d"]
     a    b
c  4.0  1.0
d  2.0  2.0
>>> a[:"d"]
     a    b
c  4.0  1.0
d  2.0  2.0
>>> a["d":"d"]
     a    b
d  2.0  2.0

Dataframeにおいて辞書型のように列インデックスもしくはカラム名でアクセスできるが, 行の場合はそうはいかないっぽい. スライスならばインデックス名を使えるが,辞書型のように単独のインデックス名を指定して行を切り出すことはできないっぽい. 行の場合はa["d":"d"]というように同じインデックス名を指定したスライスで指定の一行を得るといったトコロか...

要素の抽出・指定に関してはlocilocixというメソッドでもできる. とりあえずここでは辞書型的なアクセスについてまとめた.

CSVだけでなくHTMLテーブル出力もできる!

>>> a = pd.DataFrame({"":{"a":1,"b":2}})
>>> with open("test.html",mode="w",encoding="utf-8") as f:
...     f.write(a.to_html())
...     
271

を実行すると以下の内容のtest.htmlというファイルが指定のディレクトリ下に作成される.

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>a</th>
      <td>1</td>
    </tr>
    <tr>
      <th>b</th>
      <td>2</td>
    </tr>
  </tbody>
</table>
a 1
b 2

っとこんな感じの表ができ,マークダウンにコピペして埋め込められるから便利~

ただ,SeriesでやろうとするとAttributeError: 'Series' object has no attribute 'to_html'エラーが出るのでSeriesにはこのメソッドがなく,DataFrameでしか使えないみたいなんですよね. なのでSeriesをどうしてもHTMLテーブル出力したい場合は以上の例のように, 空のカラム名(ヘッダー)を作成してDataFrameにした上でto_html()を使うといったトコロでしょうねぇ.

また,to_html()はHTMLテーブル変換した文字列を作成するだけ(文字列を返却するメソッド)なので, ファイル出力するにはwith構文ファイルを作成して書き込まなければいけません. to_csvのように出力パスを引数に渡してメソッド単体で出力までできないです.

DataFrameからグラフプロット

最初にやっておくこと

プロットして出力・保存するにはmatplotlibの力が必要. 以下をスクリプトの最初に書いておくと良いと思われる. そして,PandasのDataFrameで日本語を扱っている場合デフォルトのフォントだと文字化けするので以下のように変更する必要がある.

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties 
fp = FontProperties(fname=r'C:\WINDOWS\Fonts\msgothic.ttc',size=16) 
plt.rcParams["font.family"] = 'Yu Gothic'
plt.rcParams["figure.figsize"] = (15, 15)

上記のfp = FontProperties(fname=r'C:\WINDOWS\Fonts\msgothic.ttc',size=16)はプロットをmatplotlibのメソッドを使って行う場合, 例えば日本語記述の凡例を示す場合に

plt.legend(loc="upper right", fontsize=10,prop=fp)

というように引数として使用するが,plt.rcParamsでデフォルトフォントを変更すれば必要ないかも. 適宜目的に応じて書いたり書かなかったりすればいい部分だと思われる. 日本語文字化け回避で重要なのはplt.rcParamsで日本語を扱えるフォントをデフォルトに変更しておくこと.

自身の環境下のmatplotlibで扱えるフォントは

>>> import matplotlib.font_manager as fm
>>> fonts = fm.findSystemFonts() 
>>> print([[str(font), fm.FontProperties(fname=font).get_name()] for font in fonts])

を実行すると示されるので,日本語が扱えそうなフォントの名前をコピペしてplt.rcParamsに指定すればおk.

ここではJupyter Notebookのハナシは省略.

plt.rcParams["figure.figsize"] は描画される図のサイズの変更. 最初の第一引数が横の長さで,第二引数が縦の長さの指定になる. あんまりに小さいと目盛りとか省略されるのでデータが大きいほど大きくしといたほうが良いと思われる.

DataFrameをプロット

以下dataにはDataFrameが代入されていると仮定してスクリプトを見てください.

plt.figure()
data.plot()
plt.grid(True)
plt.xticks(ticks=[i for i in range(len(data.index))],labels=data.index,rotation=90)
plt.savefig(title+".png")
plt.close('all')

以上で折れ線グラフがPNGファイルとして出力される. デフォルトでは折れ線グラフなので, data.plot(kind="bar")というように引数kindを指定すると出力されるグラフの形状が変わる. ちなみにbarで棒グラフ.

まずplt.figure()で描画領域の確保?だっけかな. これは最初に一回やっておけばいい.
んで,次にPandasのDataFrame側のメソッドを使ってグラフを描画.
plt.grid(True)でグラフに格子が表示されるようになる. いらない場合は記述しなくてよい.
plt.xticksはX軸に関してグラフが描画された後に変更するもので, ticksは指定したリストの数だけ目盛りを表示し, labelsがその目盛りに対して指定したリストを配置してラベル表示させ, rotationで目盛りに示されたラベル(文字)を指定した分だけ回転させる,といった具合. 実際やってみると分かるかと思う. plt.xticks(ticks=[i for i in range(len(data.index))],labels=data.index,rotation=90)を記述しておくことで 通常省略されるところをインデックス全て表示されるようになる. ただし,インデックスが多いほど見にくくなったり,隣と重なって文字がつぶれてしまうこともあるので 状況に応じて使おう.
plt.savefigで画像として出力している.plt.show()スクリプト内で表示だったかな.
plt.close('all')で描画のリセット.スクリプト内で複数グラフを描画する場合は, 次のグラフを描画する前にやっておく必要がある. でないと前のグラフの描画が残ったままでグラフが重なってしまう可能性がある.