Pandas, データフレームの扱い

0. まず,

cmd

> poetry new k02
> cd k02
> poetry env use py
> poetry install --sync
> poetry add ipykernel

により,いつものように,パッケージ開発用フォルダを作り, その下に,仮想環境(.venv),配布するフォルダ(k02)などを作る.

0.1. 外部パッケージの利用

今回は,パッケージの中で,外部パッケージを使いたいので,

> poetry add pandas
> poetry add openpyxl
> poetry add matplotlib
> poetry add numpy

というように,

  • Pandas
  • OpenPyXL
  • MatplotLib
  • NumPy

の4つの外部パッケージを仮想環境にインストールしておきます.

0.2. リソースの利用

パッケージには,フォルダ,モジュール(.py)の他に,「リソース」と呼ばれる資源を含ませることができます.(つまり,パッケージの一部として配布されます.)

配布パッケージ(フォルダ) k02 の下の data というフォルダにリソースファイル mydata1.csv を置いているとします.

この mydata1.csv の「ファイルパス」は,

from importlib import resources

# 'k02/data/__init__.py' があればフォルダである'data'をimportできる.
from k02 import data

# `resources.files(foo)` は,importしたモジュール'foo'のファイルパス
data_path = resources.files(data)
print(data_path)

# .joinpath(パス)は,パスを後ろに結合する.
fpath = data_path.joinpath('mydata1.csv')
print(fpath)
MultiplexedPath('/Users/kotaro/MyRepository/MyLecture/psp2/2024psp2/d06/k02/data')
/Users/kotaro/MyRepository/MyLecture/psp2/2024psp2/d06/k02/data/mydata1.csv

今回は,4 つのcsvファイルをリソースにします.

0.3. vscode で jupyter

vscodeで配布用フォルダ k02 の下に,k02_1.ipynb という名前のファイルを作り,開きます.

1. Pandas

1.1 標本の追加

最初のセルに,

import pandas as pd

# アジア語の文字幅をきれいに表示するおまじない
pd.set_option('display.unicode.east_asian_width', True)

と書いて,実行します.

次に,データフレームを作ります.データフレームには変数(項目名)が必要です.

df = pd.DataFrame(columns=['名前','性別','好きな数','誕生月','セロリ好き度','身長'])

dfを表示するには,普通はprint(df)としますが,jupyterでは,display(df)とすると,きれいに表示されます.

display(df)
名前 性別 好きな数 誕生月 セロリ好き度 身長

今はデータがありませんので,項目名のみが表示されます.

では,データを入れていきましょう.

というか,データが1つでもデータフレームです.

sample_0 = {
    '名前':'鈴木', '性別':'女', '好きな数':3,
    '誕生月':12, 'セロリ好き度':'好き', '身長':165
    } # dictで作ります.
df_0 = pd.DataFrame(data=[sample_0])
display(df_0)
名前 性別 好きな数 誕生月 セロリ好き度 身長
0 鈴木 3 12 好き 165

別の標本は,

sample_1 = {
    '名前':'薗田', '性別':'男', '好きな数':7,
    '誕生月':5, 'セロリ好き度':'とても嫌い', '身長':172
    }
df_1 = pd.DataFrame(data=[sample_1])
display(df_1)
名前 性別 好きな数 誕生月 セロリ好き度 身長
0 薗田 7 5 とても嫌い 172

これら2つのデータフレームを合成して,新たなデータフレーム df を作ります.

df = pd.concat([df_0, df_1], ignore_index=True, axis=0)
display(df)
名前 性別 好きな数 誕生月 セロリ好き度 身長
0 鈴木 3 12 好き 165
1 薗田 7 5 とても嫌い 172

axis=1 は,合成の方向です.標本を追加するので縦方向に合成です.縦方向はaxis=0です.(列方向,項目を追加する場合は,axis=1

1.2. 標本セットの読み込み

CSVファイルの読み込みは,read_csv関数です.

fpath = data_path.joinpath("mydata1.csv") # data_pathはリソースファイルのあるフォルダ(既に設定済)
df = pd.read_csv(fpath, skipinitialspace=True)
display(df)
# mydata1.csv
名前 性別 好きな数 誕生月 セロリ好き度 身長
薗田 7 5 とても嫌い 172
鈴木 3 12 好き 165
斎藤 4 10 嫌い 180

skip-initial-spaceはアジア語で余計な空白が入るのを防ぐので,いつもつけておくとよい.

type(df)
pandas.core.frame.DataFrame

dfDataFrameというクラスに割当てられていることがわかります.

df.shape データフレームの行数(観測数)と列数(変数数)をタプルで表示します.

print(df.shape)
(4, 1)

4つの観測,1つの変数となりました.

あれれ,それは違います.今回のデータは3観測(薗田と鈴木と斎藤),6変数(名前,…, 身長)のはずなので(3,6)であるはずです.

ファイルを直接見ると,1行目に# mydata1.csvとあり,これはメモですね.読み飛ばす行番号をリストにしてskiprowsオプションに渡して読み直します.

fpath = data_path.joinpath('mydata1.csv')
df = pd.read_csv(fpath,skipinitialspace=True, skiprows=[0])
df
名前 性別 好きな数 誕生月 セロリ好き度 身長
0 薗田 7 5 とても嫌い 172
1 鈴木 3 12 好き 165
2 斎藤 4 10 嫌い 180
df.shape
(3, 6)

きちんと,3観測6変数になりました.ちなみに読み飛ばした後の最初の行をヘッダ(header)と呼んでおり,変数名のリストとして読みます.

クラス変数 columns に変数名が保存されています.

ちなみに,ヘッダ行は観測ではないので,(4,6)ではなく(3,6)になっています.

df.columns
Index(['名前', '性別', '好きな数', '誕生月', 'セロリ好き度', '身長'], dtype='object')

変数名が並んだ行が無いCSVもあります.例えばmydata2.csvmydata1.csvと同じ3観測6変数のデータですが,普通に読むと,

fpath = data_path.joinpath('mydata2.csv')
df = pd.read_csv(fpath,skipinitialspace=True)
df
薗田 7 5 とても嫌い 172
0 鈴木 3 12 好き 165
1 斎藤 4 10 嫌い 180
df.shape
(2, 6)

のように,データの1観測目がヘッダ(変数名のリスト)と捉えられてしまいます.よって, headerが無いことを教えて read_csv します.( header=None )

fpath = data_path.joinpath('mydata2.csv')
df = pd.read_csv(fpath,skipinitialspace=True,header=None)
df
0 1 2 3 4 5
0 薗田 7 5 とても嫌い 172
1 鈴木 3 12 好き 165
2 斎藤 4 10 嫌い 180

ただし,ヘッダが無いので変数名が仮に0,….,5となっています.

df.columns
Index([0, 1, 2, 3, 4, 5], dtype='int64')

きちんと変数名を決めるには このクラス変数columnsを正しいもので上書きします.

df.columns=['Name','Sex','FavoriteNumber','BirthMonth','CeleryFavor','Height']
df
Name Sex FavoriteNumber BirthMonth CeleryFavor Height
0 薗田 7 5 とても嫌い 172
1 鈴木 3 12 好き 165
2 斎藤 4 10 嫌い 180

変数名を変えるにはrenameメソッドを使ってもよいです.

df.rename(columns={'Sex':'Gender'})
Name Gender FavoriteNumber BirthMonth CeleryFavor Height
0 薗田 7 5 とても嫌い 172
1 鈴木 3 12 好き 165
2 斎藤 4 10 嫌い 180

ところで,データフレームを表示したときに,左端に毎回0から始まる数字が書かれています. これは,CSVにおける観測の個体IDであり,「インデックス(index)」と呼んでいます.データ本体には含まれません.

print(df.index)
RangeIndex(start=0, stop=3, step=1)

今, index は0から始まって,1ずつ増えて,3の前までということみたいですね.

1.3. データを集めたら

そんなこんなで,mydata1.csvを読みます.

fpath = data_path.joinpath('mydata1.csv')
df = pd.read_csv(fpath,skipinitialspace=True,header=1)
df
名前 性別 好きな数 誕生月 セロリ好き度 身長
0 薗田 7 5 とても嫌い 172
1 鈴木 3 12 好き 165
2 斎藤 4 10 嫌い 180

1.4. データフレーム上の値の参照

データフレームの参照(抽出)は,2種類あります.参照の出力もデータフレームです.

データフレームの値の参照
  • .loc[index名リスト, 変数名リスト] : index名リスト,変数名リストによる参照
  • .iloc[行番号リスト, 列番号リスト] : データのi行目リスト,j列目の指定による参照

取り出されたものの型は DataFrame

いずれの場合も リスト なので,

[1,3,2]とか["性別",'誕生月'] のように四角カッコの配列で指定します.

# .loc
df.loc[[0, 1], ["好きな数"]]
好きな数
0 7
1 3
# .iloc
df.iloc[[0, 1], [2]]
好きな数
0 7
1 3

配列要素が数値(数字ではなく数値ね)の場合は,rangeでリストを作ることもできます.(内側の四角カッコは無し)

range(start,stop,step)[start+0*step, start+1*step, ..., ] < stop を作るのでした.

df.iloc[range(0, 1), range(2, 5, 2)]
好きな数 セロリ好き度
0 7 とても嫌い

range で作るものは,スライスと呼ばれる形で表現できます.

df.iloc[0:1, 2:5:2]
好きな数 セロリ好き度
0 7 とても嫌い

1.4.1. ある1つの変数だけ取り出す.

上の例でも1つの変数「好きな数」を取り出していましたが,全てのサンプルにおけるある1つの変数だけを見たいときがあります.

locilocでもいいですが,データフレームになっています.場合によっては値のリストとして取り出したいときがあります.

列データを取り出す

df['変数名']: 変数名 の列を取り出す

取り出されたものの型は Series

display(df["好きな数"])
print(df["好きな数"].shape, type(df["好きな数"]))
0    7
1    3
2    4
Name: 好きな数, dtype: int64
(3,) <class 'pandas.core.series.Series'>

表示がきれいではなくなりました.型(クラス)が Seriesとなっているからです.

Seriesは,DataFrameのある変数を取り出したものになっていて,データ(値のリスト)のサイズが (3,) つまり,3要素のリストになっています.また,DataFrameと同様に index を持ちます(今の場合,0, 1, 2index).さらに Nameに変数名を持ちます.

また,データは普通のリストではなく, 全ての要素が同じ型になっている特別なリスト です(変数というのはそういうもの).今の場合,dtype: int64 つまり 64bit整数 という型です.

Seriesはリストと同様に順番のあるコンテナなので,順番を指定することで要素を取り出せます.

サンプル0番目と1番目は,

df["好きな数"][[0, 1]]
0    7
1    3
Name: 好きな数, dtype: int64

という感じです.上のほうでも,あるサンプルのある変数の値を取り出しましたが,上のほうではDataFrameを取り出しましたが,今回はSeriesを取り出しています.

またSeriesのままだと扱いにくい場合には,list に変換できます.

df["好きな数"].to_list()
[7, 3, 4]

1.5. データの要約

要約とは,変数ごとに分布の特徴(統計量)に代表する処理です.

データの要約は describeメソッドで行います.

df.describe()
好きな数 誕生月 身長
count 3.000000 3.000000 3.000000
mean 4.666667 9.000000 172.333333
std 2.081666 3.605551 7.505553
min 3.000000 5.000000 165.000000
25% 3.500000 7.500000 168.500000
50% 4.000000 10.000000 172.000000
75% 5.500000 11.000000 176.000000
max 7.000000 12.000000 180.000000

値が数値型の変数のみについて要約されました.

  • countはデータの個数(sample size,サンプルサイズ)
  • meanは平均値,std標本 の標準偏差(STandard Deviation)
  • minは最小値,maxは最大値,
  • 25%, 50%, 75%は四分位で,ヒストグラムを低い水準から並べたときに下位25%,50%,75%にあたる水準を答えるものです.下位50%はmedian(メディアン,メジアン)とも呼ばれます.

この関数の中身は,以下のDataFrameのメソッドを使っても表示されます.

# 各変数のサンプルサイズ
df['身長'].count()
3
# 各変数の標本平均
df['身長'].mean()
172.33333333333334
# 各変数の標本中央値
df['身長'].median()
172.0
# 各変数の標本標準偏差
df['身長'].std(ddof=0)
6.128258770283411
# 第1四分位
df['身長'].quantile(q=0.25)
168.5
# 第2四分位(中央値)
df['身長'].quantile(q=0.5)
172.0
# 第3四分位
df['身長'].quantile(q=0.75)
176.0

1.6. 変数の尺度を正しく設定

ところで,身長は平均や標準偏差に意味がありますが,「好きな数」や「誕生月」にとっての平均や標準偏差は 意味不明 ですね.

「好きな数」や「誕生月」って,いわば「性別」と同じように,どれが最初でどれが最後だとか無いですね.

また,さきほどの「要約」で,変数「セロリ好き度」は単なる5段階評価値「とても嫌い,嫌い,普通,好き,とても好き」で選んでいるので,代表値や分布を考えられそうですが,文字なので「要約」ができませんでした.

ということで,各変数は,尺度(スケール,scale)を設定しなければいけません.(pandasに教えてあげなければなりません)

尺度名 特徴 計算可能
名義尺度(nominal) 分類のための単なる文字・数字 ==, !=, is, not
順序尺度(ordered) 順序関係のある文字・数字 >, <
間隔尺度(interval) MKS単位系でない測定値 +,-
比尺度(ratio) MKS単位系の測定値 *,/

間隔尺度と比尺度は見分けにくいですが,MKS単位系(メートル,キログラム,秒)を基本単位とする測定値は原器に対する比で表されるので比尺度,それ以外の単位系(または単位無し)の測定値は間隔尺度と考えられます.

では,mydata1.csv の各変数の尺度は何でしょうか.

また,各変数は,とりうる値のリスト(「水準」といいます)がすでに決められています.各変数の具体的な水準はどうなっているでしょうか.

変数名 尺度 水準
名前 名義尺度 あらゆる人名
性別 名義尺度 集合{‘男’,‘女’}
好きな数 名義尺度 集合{‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’,‘10’,‘11’,‘12’}
誕生月 名義尺度 集合{‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’,‘10’,‘11’,‘12’}
セロリ好き度 順序尺度 リスト[‘とても嫌い’,‘嫌い’,‘普通’,‘好き’,‘とても好き’]
身長 比尺度 あらゆる実数

Pandasでは,それぞれの尺度に型(クラス)を割り当てています.

  • 名義尺度は,Categorical(categories=[...],ordered=False)クラス
  • 順序尺度は,Categorical(categories=[...],ordered=True)クラス
  • 間隔尺度と比尺度は,Floatクラス

というわけで,そのように直します.

公式サイトのclass pandas.Categorical

  • 「名前」(名義尺度)

    尺度を設定する前

    dtype に,現在の型(クラス)が表示されますので,注目してください.

df['名前']
0    薗田
1    鈴木
2    斎藤
Name: 名前, dtype: object
尺度を設定する前のdtypeは,`object`.object型というのは,汎用型(いわば不明)であり,所謂文字列型(str)と同義です.

名義尺度に設定.水準を全て挙げることができないので,`categories` は指定できない.
df['名前'] = pd.Categorical(df['名前'])
df['名前']
0    薗田
1    鈴木
2    斎藤
Name: 名前, dtype: category
Categories (3, object): ['斎藤', '薗田', '鈴木']
dtypeが category になりました.水準は3種です.
  • 「性別」(名義尺度)

    尺度を設定する前

df['性別']
0    男
1    女
2    男
Name: 性別, dtype: object
名義尺度に設定.水準がすべて挙げられるなら `categories` に水準の集合を指定する.
GenderSet = {'男','女'}
df['性別'] = pd.Categorical(df['性別'], categories=GenderSet, ordered=False)
df['性別']
0    男
1    女
2    男
Name: 性別, dtype: category
Categories (2, object): ['女', '男']
  • 「好きな数」,「誕生月」(名義尺度に設定)

    尺度を設定する前

    dytpeは,数なので int64.64ビット整数として読み取られています.

df['好きな数']
0    7
1    3
2    4
Name: 好きな数, dtype: int64
名義尺度に設定.

ここで測定した数は,単なる選択肢で名義尺度なので,「数字」(文字)に直す.

dtypeは文字列,`object`型になります.
df['好きな数'] = df['好きな数'].astype('str')
df['好きな数']
0    7
1    3
2    4
Name: 好きな数, dtype: object
また,水準を全て挙げることができないので,`categories`は指定できない.

これで,dtypeは正しく category になります.
df['好きな数'] = pd.Categorical(df['好きな数'], ordered=False)
df['好きな数']
0    7
1    3
2    4
Name: 好きな数, dtype: category
Categories (3, object): ['3', '4', '7']
次に,「誕生月」.尺度を設定する前は,
df['誕生月']
0     5
1    12
2    10
Name: 誕生月, dtype: int64
これも大小・優劣など無いので,名義尺度であり,文字列化する.

また,全ての水準を挙げられるので,`categories`を指定する.
df["誕生月"] = df["誕生月"].astype("str")
# MonthSet = {'1','2','3','4','5','6','7','8','9','10','11','12'} でもいいですが
MonthSet = set([str(n) for n in range(1, 13)])
df["誕生月"] = pd.Categorical(df["誕生月"], categories=MonthSet, ordered=False)
df["誕生月"]
0     5
1    12
2    10
Name: 誕生月, dtype: category
Categories (12, object): ['5', '7', '2', '12', ..., '4', '11', '6', '8']
  • 「セロリ好き度」(順序尺度)

    セロリ好き度は,強弱・大小があるので,「順序尺度」.

    尺度を設定する前は,

df['セロリ好き度']
0    とても嫌い
1          好き
2          嫌い
Name: セロリ好き度, dtype: object
全ての水準を挙げられるので,`categories` を指定.

また水準には順序があるので,リストに順に水準を並べる.`ordered = True` を指定.

dtype が正しく category になります.また,水準に順位が付いてますね.
CerelyFavorLevel = ['とても嫌い','嫌い','ふつう','好き','とても好き']
df['セロリ好き度'] = pd.Categorical(df['セロリ好き度'],categories=CerelyFavorLevel, ordered=True)
df['セロリ好き度']
0    とても嫌い
1          好き
2          嫌い
Name: セロリ好き度, dtype: category
Categories (5, object): ['とても嫌い' < '嫌い' < 'ふつう' < '好き' < 'とても好き']

あらためて要約しなおし

すべての変数の尺度を設定したので,もう一度「要約」してみましょう.

df.describe()
身長
count 3.000000
mean 172.333333
std 7.505553
min 165.000000
25% 168.500000
50% 172.000000
75% 176.000000
max 180.000000

名義尺度である「好きな数」「誕生月」が消えました.ですが,順序尺度である「セロリ好き度」は依然,出てきません.

値が数値でないと出てこないようです.考えてみると,順序尺度とはいえ,平均値や標準偏差などは数値でしか表示できませんね.水準が文字列なので無理ってことです.

1.7. 文字変数 を 数字 に対応付け

変数が文字列のものは,数字と対応付けすることができます.(se.map(dict))

1.7.1. 名義尺度

性別に対して,

  • 男: -1
  • 女: +1

に対応付けしたいならば,まず対応を記した辞書を作り,それをマッピングします.

マッピングしたのちも,尺度は変わらず名義尺度(Categorical, ordered=False)となっています.

数字だからといって大小をつけられるわけではありません.

SexMap = {'男':-1, '女':+1}
df['性別'] = df['性別'].map(SexMap)
df['性別']
0   -1
1    1
2   -1
Name: 性別, dtype: category
Categories (2, int64): [1, -1]

1.7.2. 順序尺度

同様に,「セロリ好き度」も数字にしましょうか.

以下のイメージです.

とても嫌い(-2) < 嫌い(-1) < ふつう(0) < 好き(+1) < とても好き(+2)
CerelyFavorMap = {
    "とても嫌い": -2,
    "嫌い": -1,
    "ふつう": 0,
    "好き": +1,
    "とても好き": +2,
}
df["セロリ好き度"] = df["セロリ好き度"].map(CerelyFavorMap)
df["セロリ好き度"]
0   -2
1    1
2   -1
Name: セロリ好き度, dtype: category
Categories (5, int64): [-2 < -1 < 0 < 1 < 2]

ただし,mapで対応付けしても,文字と数字の対応がされるだけで,数値としては扱われず,大小は評価されません.(dtype: category)

しかた無いので,順序尺度でなく間隔尺度に設定しなおします. (つまりは数字を数値にする)

df['セロリ好き度'] = df['セロリ好き度'].astype(int)
df['セロリ好き度']
0   -2
1    1
2   -1
Name: セロリ好き度, dtype: int64

あらためて「要約」

あらためて,要約します.

df.describe()
セロリ好き度 身長
count 3.000000 3.000000
mean -0.666667 172.333333
std 1.527525 7.505553
min -2.000000 165.000000
25% -1.500000 168.500000
50% -1.000000 172.000000
75% 0.000000 176.000000
max 1.000000 180.000000

1.8. 名義尺度の再解釈

\(\text{性別}\in\{\text{男性},~\text{女性}\}\) は,名義尺度でした.

ですが,このような2値データの場合,\(\text{男性か?}\in\{\text{Yes},~\text{No}\}\) と解釈し直すこともできます.このような Boolな回答(Yes/No, True/False)は,間隔尺度と考えるのがよいとされています.(名義尺度は計算できないが,間隔尺度は計算できる)

その流れで,2つ以上のカテゴリ水準となる1列の名義尺度は,複数列のBool回答(つまり間隔尺度)に直せます.

例えば

s1 = {"名前": "薗田", "所属学部": "情報データ科学部"}
s2 = {"名前": "鈴木", "所属学部": "工学部"}
s3 = {"名前": "中村", "所属学部": "水産学部"}
df6 = pd.DataFrame([s1, s2, s3])
df6
名前 所属学部
0 薗田 情報データ科学部
1 鈴木 工学部
2 中村 水産学部

所属学部は,名義尺度で,カテゴリは,

categories={"情報データ科学部","工学部","医学部","歯学部","薬学部","環境科学部","水産学部","経済学部","教育学部","多文化社会学部"}

したがって,

df6["所属学部"] = pd.Categorical(df6["所属学部"], categories=categories, ordered=False)
display(df6)
print(df6["所属学部"])
名前 所属学部
0 薗田 情報データ科学部
1 鈴木 工学部
2 中村 水産学部
0    情報データ科学部
1              工学部
2            水産学部
Name: 所属学部, dtype: category
Categories (10, object): ['環境科学部', '経済学部', '情報データ科学部', '教育学部', ..., '医学部', '水産学部', '工学部', '多文化社会学部']

ここで,「所属学部は?」という変数ではなく,「情報データ科学部に所属?」,「工学部に所属?」,…というように複数のBool変数に分けることができて

df_faculties = pd.get_dummies(df6['所属学部']).astype(int)
display(df_faculties)
環境科学部 経済学部 情報データ科学部 教育学部 歯学部 薬学部 医学部 水産学部 工学部 多文化社会学部
0 0 0 1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 1 0
2 0 0 0 0 0 0 0 1 0 0

のような変数に変更できます.

結果的には,

df7 = pd.concat([df6.drop(columns=["所属学部"]), df_faculties], axis=1)
df7
名前 環境科学部 経済学部 情報データ科学部 教育学部 歯学部 薬学部 医学部 水産学部 工学部 多文化社会学部
0 薗田 0 0 1 0 0 0 0 0 0 0
1 鈴木 0 0 0 0 0 0 0 0 1 0
2 中村 0 0 0 0 0 0 0 1 0 0

1.9. 整然データへ変換

1.9.1. pivot

同じデータですが,場合によっては,次のような表でまとめているかもしれません.

fpath = data_path.joinpath("mydata3.csv")
df2 = pd.read_csv(fpath, skipinitialspace=True, skiprows=[0])
df2
名前 変数名 回答
0 薗田 性別
1 薗田 好きな数 7
2 薗田 誕生月 5
3 薗田 セロリ好き度 とても嫌い
4 薗田 身長 172
5 鈴木 性別
6 鈴木 好きな数 3
7 鈴木 誕生月 12
8 鈴木 セロリ好き度 好き
9 鈴木 身長 165
10 斎藤 性別
11 斎藤 好きな数 4
12 斎藤 誕生月 10
13 斎藤 セロリ好き度 嫌い
14 斎藤 身長 180

この「回答」という列ですが,尺度は何でしょうか?

あるときは「男」や「とても嫌い」などの文字列,あるときは「7」や「172」などの数字になっていて,なんとも言えません.

このような尺度を決めきれないような列のある表は「雑然」としています.よって「整然」データに変換します.

よく見ると「変数名」という列の値の種別ごとに表を分けると,

  • 変数名==性別の表
名前 回答
薗田
鈴木
斎藤
  • 変数名 == 好きな数の表
名前 回答
薗田 7
鈴木 3
斎藤 4

というように,それぞれの表で回答の列がある尺度や分布に統一できそうです.

むしろ,「回答」という列名ではなく,それぞれの表の名前(つまり元の変数名の値)を列名にすればよいのではないでしょうか.

この作業を ピボット といいます.

pandas では,pivot 関数を使います.変数名が並ぶ列と,回答が並ぶ列を指定し,それ以外の列をindexに指定すると,変換できます.

df3 = pd.pivot(df2, columns="変数名", values="回答", index=["名前"])
df3
変数名 セロリ好き度 好きな数 性別 誕生月 身長
名前
斎藤 嫌い 4 10 180
薗田 とても嫌い 7 5 172
鈴木 好き 3 12 165

1.9.2. from wide to long (melt)

こんなデータがあったとします.

Code
import pandas as pd

df_univ = pd.DataFrame(
    {
        "ID": ["鈴木", "鈴木", "鈴木", "中村", "中村", "中村", "高橋", "高橋", "高橋"],
        "Q": [
            "志望大学",
            "入学大学",
            "出身",
            "志望大学",
            "入学大学",
            "出身",
            "志望大学",
            "入学大学",
            "出身",
        ],
        "A": ["長大", "長大", "長崎", "東大", "長大", "東京", "長大", "京大", "長崎"],
    }
)
df_univ
ID Q A
0 鈴木 志望大学 長大
1 鈴木 入学大学 長大
2 鈴木 出身 長崎
3 中村 志望大学 東大
4 中村 入学大学 長大
5 中村 出身 東京
6 高橋 志望大学 長大
7 高橋 入学大学 京大
8 高橋 出身 長崎

A列の尺度がバラバラで,Qによって分けると良さそうです.

df_univ = df_univ.pivot(columns="Q", values="A", index="ID")
df_univ = df_univ.reset_index(["ID"])
df_univ
Q ID 入学大学 出身 志望大学
0 中村 長大 東京 東大
1 鈴木 長大 長崎 長大
2 高橋 京大 長崎 長大

志望大学列と入学大学列は,尺度も分布も同一なので,これらの列名は,変数名ではなく値と考えられます.(wide型です.)

long型に直します.wide型(同一尺度・分布の列が複数ある表)をlong型(同一尺度・分布の列が1列)にするには,melt関数を使います.

df_univ = df_univ.melt(id_vars=["ID", "出身"])
df_univ
ID 出身 Q value
0 中村 東京 入学大学 長大
1 鈴木 長崎 入学大学 長大
2 高橋 長崎 入学大学 京大
3 中村 東京 志望大学 東大
4 鈴木 長崎 志望大学 長大
5 高橋 長崎 志望大学 長大

列名に適切な名前をつける

df_univ.columns = ["ID", "出身", "入学と志望", "大学名"]
df_univ
ID 出身 入学と志望 大学名
0 中村 東京 入学大学 長大
1 鈴木 長崎 入学大学 長大
2 高橋 長崎 入学大学 京大
3 中村 東京 志望大学 東大
4 鈴木 長崎 志望大学 長大
5 高橋 長崎 志望大学 長大

1.10. まとめ

  1. データフレームを作る
    • ファイルから読み込む: pd.read_csv(csvファイル名, header=)
    • 手打ちで入力:pd.DataFrame(data=[dict], columns=[])
    • 標本を付け足す:pd.concat([df0,df1], ignore_index=True, axis=0)
  2. 整然化
    • ある列の尺度や分布が複数混ざり合っていて,別の列の値(カテゴリ)によって表を分けたほうが良さそうなら,その列の値(カテゴリ)が実は変数である.
      • -> ピボット: pd.pivot(df,columns=var_col, values=var_val, index=[var_id0, var_id1,...])
    • 尺度や分布が同一の列が複数ある(wide型)なら,それらの列名は実は値である.
      • -> メルト: pd.melt(df, id_vars=[var_id0, var_id1, ...])
  3. 尺度を設定
    • 質的変数(文字,数字):
      • 名義尺度:se = pd.Categorical(se, categories=[...], ordered=False)
      • 順序尺度: se = pd.Categorical(se, categories=[...], ordered=True)
        • 場合によっては間隔尺度に設定しなおす.
    • 量的変数(数値):
      • 間隔尺度・比尺度: se = se.astype('float')

1.11. クロス集計

クロス集計表を作ることもできます.クロスさせる2つの変数名を引数に並べます.

pd.crosstab(df['性別'],df['好きな数'])
好きな数 3 4 7
性別
1 1 0 0
-1 0 1 1

2. 課題k02

これは,身近の14人に性別と身長を尋ねたときの回答を集めた標本データである.k02/k02/data/heights14.csv とする.

from importlib import resources
from k02 import data

data_path = resources.files(data)
fpath = data_path.joinpath("heights14.csv")

df = pd.read_csv(fpath)
df
ID Q A
0 1 性別
1 1 身長 183.97
2 2 性別
3 2 身長 179.54
4 3 性別
5 3 身長 166.9
6 4 性別
7 4 身長 173.62
8 5 性別
9 5 身長 165.62
10 6 性別
11 6 身長 167.83
12 7 性別
13 7 身長 152.4
14 8 性別
15 8 身長 163.24
16 9 性別
17 9 身長 161.39
18 10 性別
19 10 身長 174.38
20 11 性別
21 11 身長 171.38
22 12 性別
23 12 身長 152.28
24 13 性別
25 13 身長 169.39
26 14 性別
27 14 身長 171.1

で読み取れます.ヘッダー読み取りの有無等は考えてください.

また,このデータは,尺度が混ざっている列があるので,ピボットが必要です.

# columnsを変数名が並んでる列名,今回は "Q"
# valuesを各変数の値が並んでる列名,今回は "A"
# indexを固定する列名のリスト,今回は ["ID"]
# としてピボット
df2 = df.pivot(columns="Q", values="A", index=["ID"])
display(df2)
print(df2.dtypes)
Q 性別 身長
ID
1 183.97
2 179.54
3 166.9
4 173.62
5 165.62
6 167.83
7 152.4
8 163.24
9 161.39
10 174.38
11 171.38
12 152.28
13 169.39
14 171.1
Q
性別    object
身長    object
dtype: object

pivotされましたが,変数のtypeがobject(なにがし)になっています. このままだと,統計量が正しく計算できません(特に身長), よって,次に尺度を正します.

変数 性別 は,

  • 値は
  • 名義尺度

なので,categoryに直します.categories={'男','女'}, ordered=Falseです.

変数 身長 は,

  • 値は,実数
  • 比尺度

なので,floatに直します.

df2['性別'] = pd.Categorical(df2['性別'], categories={'男','女'}, ordered=False)
df2['身長'] = df2['身長'].astype(float)
display(df2)
print(df2.dtypes)
Q 性別 身長
ID
1 183.97
2 179.54
3 166.90
4 173.62
5 165.62
6 167.83
7 152.40
8 163.24
9 161.39
10 174.38
11 171.38
12 152.28
13 169.39
14 171.10
Q
性別    category
身長     float64
dtype: object

きちんと尺度が正されました.

k02_1

pandasパッケージを用いて,csvファイルを読み込んでデータフレームを作成し, この標本集団における男性と女性それぞれの「平均」と「標準偏差」を表示する関数を k02_1.py の中で main() として定義せよ.

開発手順は,

  1. k02/k02_1.ipynb を作り,Jupyter形式で,入力セルと出力セルを確認しながらプログラミング
  2. k02/k02_1.ipynbk02/k02_1.py にエクスポートする.入力セルのみが .py にコピーされる.
    • jupyter形式ではないため display() は使えないので消す.
    • poetry run python k02/k02_1.pyで確認.
  3. k02_1.py をモジュールに書き換える.
    • import行のあとのすべての処理を def main(): ブロックに入れる.
    • k02_1.py の末尾に,if __name__ == '__main__': main() という行を追加
  4. 動作確認用にtests/test_k02_1.pyを書いて,正しく動作することを確認
from k02 import k02_1

k02_1.main()

k02_1.pyを以下のように,def main() に処理,if __name__ == '__main__': main() を末尾に設定,とすると,

import pandas as pd

def main():
    # ここに処理を書く(インデントに気をつけよ)

if __name__ == '__main__': main()
> poetry run python k02/k02_1.py

は,

> poetry run python
>>> from k02 import k02_1
>>> k02_1.main()

実行結果は

==男==
平均:173.90 cm
標準偏差: 5.06 cm

==女==
平均: ◯ cm
標準偏差: ◯ cm

と表示されるようにせよ.(男について表示される数値は上記になるのが正解)

ちなみに,標本分散は var(ddof=0),標本標準偏差は, std(ddof=0) で計算される.(いくつかのサイトで誤っていることを確認しているので,注意すること)

df_M = df2[df2["性別"] == "男"]
display(df_M)
se_M_height = df_M["身長"]
display(se_M_height)
M_std = se_M_height.std(ddof=0)  # 標準偏差
M_ustd = se_M_height.std(ddof=1)  # 不偏標準偏差
print(f"標準偏差:{M_std}\n不偏標準偏差:{M_ustd}")
M_var = se_M_height.var(ddof=0)  # 分散
M_uvar = se_M_height.var(ddof=1)  # 不偏分散
print(f"分散:{M_var}\n不偏分散:{M_uvar}")
Q 性別 身長
ID
1 183.97
2 179.54
4 173.62
6 167.83
10 174.38
11 171.38
13 169.39
14 171.10
ID
1     183.97
2     179.54
4     173.62
6     167.83
10    174.38
11    171.38
13    169.39
14    171.10
Name: 身長, dtype: float64
標準偏差:5.060863161309539
不偏標準偏差:5.410290294561703
分散:25.612335937499985
不偏分散:29.271241071428555

k02_2

このデータはある特定の14人のデータなので,別な14人で回答を集めるたびに別の標本平均が求まる.

性別それぞれ同じ人数で回答を集めたときにその標本平均が68%の確率で収まる範囲「◯±△」を推定せよ.(\(\bar{x}\pm 1\times \mathrm{SE}\))

k02_1 と同様に最終的には モジュールk02/k02_2.pyを作り,tests/test_k02_2.pyで動作確認せよ.

  • 「◯±△」の「◯」は,母集団の平均の裁量推定値と等しく,手元の標本の平均(\(\bar{x}\))と等しい.
  • また,「◯±△」の「△」は,別の標本と手元の標本との誤差であり,標本誤差\(\mathrm{SE}\)という. \(\sqrt{\frac{u^2}{N}}\) で求まる.
    • \(u^2\) は母集団の分散で,標本の分散 \(s^2\) と標本サイズ \(N\) から「推定」できる.所謂,不偏分散と呼ばれる.

      \[ u^2 = \frac{N}{N-1}s^2 \]

ちなみに,母集団の分散と標準偏差は,var(ddof=1)std(ddof=1)で計算される.(いくつかのサイトで誤っていることを確認しているので,注意すること)

実行結果は,

==男==
別の標本の平均の68%信頼区間:173.90± 1.91 cm

==女==
別の標本の平均の68%信頼区間: ◯ ± △ cm

と表示されるようにせよ.(男について表示される数値は上記になるのが正解)

ちなみに,今回は各値を小数点以下2桁までを表示している.xを小数点以下2桁までで表示する場合には,fフォーマットで文字列を作ります.fフォーマットでは,中括弧{x}でxを文字列化します.また,:のあとに,表記方法を指定します.

x = 3.141592
print(f"円周率は {x} です(通常表記).")
print(f"円周率は {x:.2f} です(小数点以下2桁表示).")
print(f"円周率は {x:.3e} です(小数点以下3桁の指数表示,つまり有効桁4桁).")
print(f"円周率は {x:.5g} です(有効桁5桁).")
円周率は 3.141592 です(通常表記).
円周率は 3.14 です(小数点以下2桁表示).
円周率は 3.142e+00 です(小数点以下3桁の指数表示,つまり有効桁4桁).
円周率は 3.1416 です(有効桁5桁).