Hyper-Positive-Diary

TWICE/遊戯王/バイオインフォ/読書記録/アニメの感想等を走り書きします。

【読書記録/レビュー】『働き方の損益分岐点』がすごい!〜社会人1年目の給料の教科書〜

コロナウイルスの影響で外出を自粛していると、読書が捗りますね笑。

さて、今回紹介するのはこの本です。 4月から社会人になる身ですが、このタイミングで巡り会えたことに心から感謝です。

人生格差はこれで決まる 働き方の損益分岐点 (講談社+α文庫) [ 木暮 太一 ]


感想(0件)

『働き方の損益分岐点』。サイバーエージェントリクルートで勤務していたこともある、木暮太一さんの本です。小暮さんはマルクスの『資本論』や、ロバート・キヨサキの『金持ち父さん貧乏父さん』に影響を受け、より充実した生活をするためにはどうすれば良いか、考えるようになったそうです。

しかし、すぐさま経営者になりバリバリ稼げるわけでもなく、、、。一般人は一般人なりの、企業に従事しながら生活水準を上げるための方法を追い求め、この本にあるような方法にたどり着いたそうです。

世にある自己啓発本や、いわゆる「不労所得を増やそう」系の本と比べて、現実的かつ実践しがいがある内容だと思います。興味がある方は是非手に取ってみて下さい。

価値と使用価値の違い

世の中では、使用価値も含めて、価値という言葉があてがわれますが、資本論では使われ方が異なります。「使用価値」とは、一言で言えば有益性で、「使用価値があるもの」とは、その商品(財であれサービスであれ)を使うことで消費者にメリットがあるものを言います。 一方、「価値があるもの」とは、商品を生産するために必要な手間やコストがかかっているものだと言うのです。

空気を例にとって考えてみましょう。人間は空気のおかげで生きていけるので、確かに「使用価値」は高いのですが、そこらへんに漂っている空気に生産コストはかかっていないと考えられるので、「価値」は低いものであると言えます。

そして資本主義の枠組みにおいて大事なことは、商品の値段のベースは、「使用価値」ではなく、「価値」に基づいて決まっているということです。

もちろん使用価値が需要と供給のバランスに影響し、値段が変動することはあります。しかし、いくらコロナショックで需要が爆発しているとはいえ、トイレットペーパー1ロールの価格が六本木のビル1棟の価格より高くなることはないでしょう。これは、いくらトイレットペーパーに「使用価値」があっても、「価値」がなければ価格が高くならない良い例です。

給料 = 報酬ではない!

さて、この本の序盤では、「給料=報酬ではない」という内容に1/3程度のボリュームが割かれています。 僕も含め、お金について知見を持っていない人は、給料は頑張って仕事をした時間に対する「報酬」だと勘違いをしがちです。 しかし、給料=報酬の関係がが成り立つとしたら、営業成績が2倍になったサラリーマンの年収が昨年の2倍になっているはずです。しかし実際の資本主義の世界ではそんなことは滅多に起きない。こんな簡単な例からも分かる通り、

給料=報酬ではないのです。

それでは、給料の正体とは一体何なのでしょうか。

給料とは、ずばり「労働」の「価値」です。 価値とは、先ほど見たように、商品を作り出すために必要な手間やコストで決まっています。「労働」を一つの商品だと思えば、給料とは、「労働」を作り出すために必要なコスト、すなわち、企業で来る日もくる日も働き続けるために必要な「再生産コスト」でしかないのです。

資本主義社会では、対して会社に貢献していない年配社員が、活力を持って仕事をし、会社に貢献している若手社員よりも多い給料をもらっていると言うことが多々あります。給料が、労働の再生産コストであることを考えれば、この点も説明がつきます。歳を重ねると、結婚や出産を通して家族が増え、生活を維持するために必要な出費が大きくなります。そのため、明日も明後日も労働をしてもらうための再生産コストが高くなっているのです。

昇給のために働くということ

ですので、「頑張って働いてたくさん給料をもらおう」という働き方が、資本論の観点からは理にかなっていないことがわかります。

頑張って働いたところで、その人の再生産コストすなわち「価値」は上がらないので、給料は一時的なボーナスによる微々たる額しか増加しないことでしょう。

しかし、多くの日本人は給料=報酬という妄想に囚われ、頑張って働けば給料が増えると思い込んでいます。だからこそ夜遅くまで残業したり、小技が陳列したビジネス書を買い込んだりするのでしょう。

そして企業から、再生産コストから逆算された「給料」を使い、仕事のストレスを解消するために不毛な飲み会に勤しみ、気づけば老後を迎える、、、。なんと恐ろしい資本主義のカラクリでしょう。

自己収益という考え方

それでは、どのようにすればこのような生活から抜けられるのでしょうか。その答えは、お金ではなく、その人にとっての「人生の利益」、すなわち「自己内利益」を最大化することにあります。

利益とは、あたりまえですが、収益から損益を引いた金額のことです。

「利益」 = 「収益」-「損益」

同様に、自己内利益とは、自己内の収益-自己内の損益カラ計算されると考えられます。この本では、自己内の利益を「満足感(昇進・昇級から得られるメリット)」、自己内の損益を、「必要経費(肉体的・時間的労力や精神的苦痛)」と定義しています。

満足感を変えず、必要経費を下げる

労働時間を減らすには限界があります。満足感を変えずに、必要経費を下げるためにこの本で強調されていることは、「精神的苦痛を感じない仕事」を選ぶことです。

給料と言う再生産コストの中には、精神的エネルギーの補填、平たく言うなればリフレッシュ用の再生産コストも含まれています。ですが、仕事にストレスがなく、リフレッシュが必要でなければ、その分収益は高くなるという理屈です。

必要経費を変えず、満足感を増やす

そして、満足感(昇進・昇級から得られるメリット)を増やすために大切なことは、自分が培ってきた能力を生かすことです。知らず知らずのうちに、前職や部活や日々の趣味などを通して、知らず知らず培ってきた土台が必ずあるはずです。それを生かして仕事をすれば、ただ頑張って仕事をしている時よりも高い場所に手が届くだろうと述べられています。

労働力の価値を上げる

また、過去に培ってきた土台を利用するのではなく、土台に積み上げを行っていくことも重要です。この土台が高ければ高いほど、その人の労働力を生産するためのコスト、すなわち「労働力の価値」が高まるので、労働力の価格に基づいて決まっている給料も自然と高くなると言うわけです。

しかし、目先の結果に目が眩んで、長期的な土台づくりに勤しむことができる人は少数です。これは、PL(損益計算書)的な思考が強く、BS(バランス)的な発想が足りていないためだと言います(『金持ち父さんのキャッシュフロー・クワドラント』を読むと、違いがよくわかる)。BS的な考え方ができれば、土台という資産を作るための時間がいかに大切か分かるはずです。

要約とまとめ

極論(かなり薄っぺらい言い方)でまとめると、

  1. 頑張って働いても、給料は上がらない。
  2. ストレスが少ない仕事を選べ
  3. 自分の能力を生かせ
  4. 自己投資で土台を築け

ということでしょうか。しかしこれはあくまで、具体例や細部の説明を交えて丁寧に説明された本の内容を、4行でまとめたものです。本来はもっと濃くて重要な部分がたくさんありますので、是非この記事だけでなく、実際にこの本を読んで、成功のための土台の一段にしてみてはいかがでしょうか。

それでは!

【Python】Rユーザーに優しいPandasの使い方

f:id:slmnphmet1-2:20200316145952j:plain

元々僕はRユーザーでしたが、修士生活を通してPythonを書かなければならない機会が増え、 今では主に、研究でもPythonを使っています。

当初、「Rでできたこの処理、Pythonでどうやるんだっけ?」 ということが度々起こり、調べては忘れ、調べては忘れを繰り返していました。 本来解析やコーディングに時間を割きたい身からしたら、 この時間はすごくタイムロスですよね。

特に当初戸惑ったのが、Rのdata.frameとPythonのPandas.DataFrameの違いです。 要素へのアクセスや結合の仕方が、Rと同じような書き方だとうまくいかず、 イライラした経験、皆さんもあるのではないでしょうか。

この記事は、僕のようなRユーザーがPandasを使ったときに、 あちこちの記事を調べなくてもスムースにコーディングを行えるように書きました。

もし皆さんにとって少しでも役に立ちそうであれば、 ご一読ください。

R-Python対応表

RとPythonで対応している部分については、こちらにまとめてあります。 ほとんどの処理はこれで事足りると思われます。

操作 R Python
csvから読み込み read.csv(path) pd.read_csv(path)
csvから読み込み(index列の指定) read.csv(path, row.names=1) pd.read_csv(path, index_col=0)
csvから読み込み(区切り文字の指定) read.csv(path, sep="\t") pd.read_csv(path, sep="\t")
上からn行 head(df, n) df.head(n)
下からn行 tail(df, n) df.tail(n)
共分散 cov(df) df.cov()
転置 t(df) df.T
行ごとの和 apply(df, 1, sum) df.sum(axis=1)
列ごとの和 apply(df, 2, sum) df.sum(axis=0)
行ごとの標準偏差 apply(df, 1, sd) df.std(axis=1)
列ごとの標準偏差 apply(df, 2, sd) df.std(axis=0)
自作関数の適用(行ごと) apply(df, 1, function(x) { ...}) df.apply(lambda x: ..., axis=1)
自作関数の適用(列ごと) apply(df, 2, function(x) { ...}) df.apply(lambda x: ..., axis=2)
列Aの値の出現回数を集計 table(df[, "A"]) df["A"].value_counts()
列Aの値を昇順で並べ替え sort(df[,"A"]) df["A"].sort_values()
列Aの値を降順で並べ替え sort(df[, "A"], decreasing=TRUE) df["A"].sort_values(ascending=False)
列Aの値でDataFrame全体を並べ替え df[order(df[,"A"]),] df.sort_values("A")
行名へのアクセス rownames(df) df.index
列名へのアクセス colnames(df) df.columns
i行目、j列目の要素へのアクセス df[i, j] df.iloc[i, j]
i1~i2行、j1~j2列へのアクセス df[i1:i2, j1:j2] df.iloc[i1:i2, j1:j2]
全行、j1~j2列へのアクセス df[, j1:j2] df.iloc[:, j1:j2]
i1~i2行、全列へのアクセス df[i1:i2, ] df.iloc[i1:i2, :]
行名a, 列名bの要素へのアクセス df["a", "b"] df.loc["a", "b"]
横方向の結合 cbind(df1, dfd2) pd.concat([df1, df2], axis=1)
縦方向の結合 rbind(df1, dfd2) pd.concat([df1, df2], axis=0)

準備

以下では、上記の表で説明が不十分だったり、RにはないPandasの便利機能だったりについて述べていきたいと思います。

今回はRのirisデータをcsvとして吐き出し、 pythonのDataFrameとして読み込み、 Pandasの解説をしていきます。

pathには自分の好きな場所を指定して下さい。

data(iris)
path <- "~/pandas_example.csv"
write.csv(iris, path)
df <- read.csv(path, row.names=1)
import pandas as pd
path = "~/pandas_example.csv"
df = pd.read_csv(path, index_col=0)
df.head()

jupyter上などで、2つカーネルを立ち上げ、ポチポチ実行しつつ 対比するとわかりやすいかもしれません。

Pythonオブジェクト指向

オブジェクト指向くらい分かっとるわ!!ボケ!!という人にとっては時間の無駄なので、躊躇なく飛ばして下さい。

オブジェクト指向とは

そもそもPandasとRのdata.frameの一番の違いは、 pythonオブジェクト指向であるという点です。 オブジェクト指向では、オブジェクト(変数だったりモジュールだったり)がattributeという変数のようなものと、 methodという関数のようなものを内部に持っています。 オブジェクト指向のメリットについては、別の記事で紹介しておりますので、そちらも参照ください。

(1. blog.hatena.ne.jp)

(2. blog.hatena.ne.jp )

Pythonにおけるattribute/methodへのアクセス

attributeにアクセスするには、 object_name.attribute_name というふうに指定します。

例えば、aという名前のlist型のオブジェクトが持っている、 __class__というattributeにアクセスしたいときは、

a = [1, 2, 3]
print(a.__class__)

と書きます。これにより、 aのクラスが何かを確認できます(もちろんlist型です)。

methodを用いたい時も同様です。 object_name.method_name() とします。()の中には、そのmethodの引数を入れます。

例えば、配列a内の「1」という数字を除去したいときは、

a.remove(1)
print(a)

とします。

オブジェクトaにどのようなattribute/methodが定義されているかは、 以下のようにして確認することができます。

dir(a)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__imul__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rmul__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'pop',
 'remove',
 'reverse',
 'sort']

配列1つとっても、たくさんのattribute/methodがありますね。

以降、このようなオブジェクト指向の仕組みが分かっているとして、 PandasのオブジェクトであるDataFrameとSeriesについて、話を進めます。

DataFrameとSeries

Rのdata.frameに慣れている人は、Pandasを使い始めた時、 Seriesの扱いについて戸惑うと思います。 ※ Seriesくらいわかっとるわボケ!!という人にとっては時間の無駄なので、躊躇なく飛ばして下さい。

DataFrameは、Rのdata.frameと同様に、 テーブルデータを扱うための型であり、 data.frameに慣れていれば直感でDataFrameをいじることができます。 しかしSeriesに関しては、厳密にはR内に対応するものがはありません。

ただし、「PandasのSeries \fallingdotseq name付きのVector」と思っておくと、イメージが湧きやすいかもしれません。

Seriesについて、押さえておきたい特徴としては、 「DataFrameの1列」であることが挙げられます。

species = df["Species"]
species.__class__

この出力は、pandas.core.series.Seriesとなるはずです。 これは、DataFrameから1列を抜き出した時、それがSeriesとして扱われることを意味します。

以上のようなことがわかれば、Seriesについての直感的な理解はつかめたと思います。

ところで、Seriesが、Rにおける「name付きのVector」と大きく異なる点は、 SeriesがNameというattributeを持っている点です。

print(species)
1         setosa
2         setosa
3         setosa
4         setosa
5         setosa
         ...    
146    virginica
147    virginica
148    virginica
149    virginica
150    virginica
Name: Species, Length: 150, dtype: object

Name: Speciesとありますが、 これは、元のDataFrameのcolumnに、 Speciesという名前で保存されていることを意味します。

逆に、Seriesオブジェクトを既存のDataFrameにくっつける時、 column名は、SeriesのNameとなります。RのVectorと同じ感覚でSeriesを使っていると、 この点でつまづくことがある(僕だけかもしれません)かと思うので、 是非覚えておきたい点です。

代表的なmethod

PandasのDataFrameは、オブジェクト指向で書かれているため、 様々なmethodが内蔵されています。

既存の組込関数をDataFrameに適用するだけでも十分な時もありますが、 methodを適用することが便利な時もあります。ですので、下で取り上げたようなものについては、 是非使いこなしたいところです。

head, tail

Rのhead関数、tail関数に対応するmethodが、 DataFrameには備わっています。

df.head()
df.tail()

defaultでは5行を表示してくれますが、 出力する行数を増やしたいときは、

df.head(10)

のように、表示したい行数を引数として指定してみて下さい。

sum, mean, std

sum/mean/stdは、それぞれ、合計/平均/標準偏差を求めるためのmethodです。 これをDataFrameに適用すると次のようになります。

df.sum()
Sepal.Length                                                876.5
Sepal.Width                                                 458.6
Petal.Length                                                563.7
Petal.Width                                                 179.9
Species         setosasetosasetosasetosasetosasetosasetosaseto...
dtype: object

default(axis=0)では、各列についての和がSeriesの形で出力されます。 Rと異なり、pythonではstr型の足し算が定義されているため、 Speciesについての和は、全文字をつなげたものになります。

各行についての和を求めたいときは、

df.sum(axis=1)

とします。

1      10.2
2       9.5
3       9.4
4       9.4
5      10.2
       ... 
146    17.2
147    15.7
148    16.7
149    17.3
150    15.8
Length: 150, dtype: float64

Rの書き方では、前者が

apply(df, 2, sum)

に、後者が

apply(df, 1, sum)

に対応すると思っておいていいでしょう。

また、Seriesについても、sumを適用することができます。

df["Petal.Length"].sum()

この場合は、Seriesの全要素についての和が返ってきます。

ところで、Rで、

sum(df[,c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")])

を行うと、data.frame内の全要素の和を返します。 これに直接対応するpandasのmethodはないので、

df[["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]].sum().sum()

と2回sumを行いましょう。

以上のような処理は、mean/stdについても同様に扱うことができます。

cov

covはDataFrameのみに定義されるmethodです。各列の分散共分散行列が返り値となります。

df.cov()
 Sepal.Length    Sepal.Width Petal.Length    Petal.Width
Sepal.Length    0.685694    -0.042434   1.274315    0.516271
Sepal.Width -0.042434   0.189979    -0.329656   -0.121639
Petal.Length    1.274315    -0.329656   3.116278    1.295609
Petal.Width 0.516271    -0.121639   1.295609    0.581006

Speciesについては他のcolumnと共分散が定義できないので、 自動で無視されます。

対応するRの関数は、

cov(df[,c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")])

もしくは

var(df[,c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")])

です。やはり、Pandasの場合は、str型が入ったcolumnを除去しなくていい点が便利ですね。

T

DataFrameを転置したいときは、

df.T

としましょう。

厳密にはTは、methodではなく、attributeになります(DataFrameオブジェクトを定義する際、転置したものも同時に保存されるんですね)。

value_counts

Speciesというcolumnには、iris(あやめ)の品種が記されています。 これらの登場回数を知りたいとします。

そんな時便利なのが、value_countsです。 value_countsは、Seriesオブジェクトに定義されますので、

df["Species"].value_counts()

としてみましょう。aとbの内訳が、

setosa        50
virginica     50
versicolor    50
Name: Species, dtype: int64

のように出力されるはずです。

これは、Rの

table(df[,"Species"])

に対応します。

sort_values

Seriesの場合

df["Petal.Length"]の値を、昇順、もしくは降順に並べ替えたいとします。

昇順であれば、

df["Petal.Length"].sort_values()

降順であれば、

df["Petal.Length"].sort_values(ascending=False)

としましょう。 これは、Rのsort関数と等価です。

sort(df[, "Petal.Length"])

DataFrameの場合

DataFrameにもsort_valuesは備わっていますが、少々使い勝手が違います。

df.sort_values("Petal.Length")

これにより、Aが昇順になるように、DataFrameの行全体が入れ替わります。 これは、order関数を使った以下のRコードと等価です。

df[order(df[,"Petal.Length"]),]

apply

Pandasのapplyは、Rのそれとほぼ同じように扱うことができます。 例えば、applyで関数sumを適用するには、以下のように書きます。

df[["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]].apply(sum,axis=0)
df[["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]].apply(sum,axis=1)

上記の処理では、DataFrameのsum methodと同じ出力を得ることができます。 しかし、applyで適用しているのは、python内のdefaultのsum関数です。これは、str型には適用できないため、Speciesのcolumnを除去する必要があります。

これだけでは、applyのありがたみの実感が薄いかと思うので、 lambdaで自作関数を適用する例を見てみましょう。

df.apply(lambda x : x["Sepal.Length"]* x["Pepal.Length"], axis=1)

この処理では、各行ごとに、Sepal.LengthとPetal.Lengthをかけた値を返してくれます。lambda x: のあとには、自分の好きな処理を書いて良いのですが、慣れてくれば自由度の高い処理を投げることができるでしょう。

Rでは以下のコードと等価ですね。

apply(df, 1, function(x) {x["Setal.Length"]*x["Petal.Length"]})

groupby

df.groupby("Species").mean()
Sepal.Length    Sepal.Width Petal.Length    Petal.Width
Species             
setosa  5.006   3.428   1.462   0.246
versicolor  5.936   2.770   4.260   1.326
virginica   6.588   2.974   5.552   2.026

groupbyは、カテゴリー変数が入ったカラムを引数にとり、カテゴリーごとに何かしらの集計を行ってくれます。

今回は、平均値を出してみました。集計用の関数はmean以外にもいくつか選択肢があります。とても便利な機能ですが、おろらくRに同様な関数は備わっていないと思われます(勉強不足だったらごめんなさい)。

plot method

DataFrame、Seriesどちらにも、plotというmethodが組み込まれています。 kind引数に、好きなplotのタイプを入れることで様々なplotを描けます。 matplotlibがbackendで動いており、ggplotのような綺麗なplotの作成が可能です。

scatter

例えば、散布図ですと、以下のように書きます。

df.plot(kind="scatter", x="Petal.Length", y="Sepal.Length", title="Scatter plot : Sepal~Petal", figsize=(15,9))

f:id:slmnphmet1-2:20200316140816p:plain

引数xには、x軸に使いたいcolumnの名前を、引数yには、y軸に使いたいcolumnの名前を指定します。引数titleには、plotの上に出力されるグラフのタイトルを、figsizeには、縦✖️横の幅をtuple型で指定します。

histrogram

ヒストグラムを書きたいときは、kind="hist"としましょう。

df.plot(kind="hist", figsize=(14,7), title="Histogram")

Rのhist(df)と異なり、各columnのヒストグラムを一緒の画面に描いてくれます。

f:id:slmnphmet1-2:20200316140831p:plain

他にもたくさんの便利なplotがありますので、興味がある人は、こちらを参照してみて下さい。

note.nkmk.me

要素へのアクセス方法

DataFrameやSeriesの各要素へのアクセス方法についてです。

column, index, name

まず、列名、行名へのアクセスの方法です。 Rの colnames(df) に対応するのは、

df.columns

です。

Index(['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width',
       'Species'],
      dtype='object')

同様に、

Rの rownames(df) に対応するのは、

df.index
Int64Index([  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,
            ...
            141, 142, 143, 144, 145, 146, 147, 148, 149, 150],
           dtype='int64', length=150)

となります。

また、Seriesの名前にアクセスしたいときには、.nameを使います。

df["Sepal.Length"].name
'Sepal.Length'

DataFrame内の要素

行番号/列番号による指定

例えば、Rで1行目の1列目にアクセスしたい時、df[1,1]と書きますが、これに対し、pythonではilocを用いて以下のように書きます。

df.iloc[0,0]

と書きます。 ilocを用いず、df[1,1]のようにしてもエラーを吐くだけなので注意が必要です。

幅を持たせたいとき、例えば、1列目から3列目を取り出したいとき、 Rではdf[,1:3]と書くのに対し、pythonでは、

df.iloc[:, :3]

と書きます。 Rでは、全ての行を取り出したいとき、[,]のカンマの左側に何も入れないのに対し、Pythonではコロンを入れます。

行名/列名による指定

行名、列名で指定したいときには、.loc[行名,列名]を用います。 例えば、個体「1」のPetal.Lengthを知りたいとき、Rではdf[1, "Petal.Length"]とします。これに対しpythonでは、

df.loc[1, "Petal.Length"]

とします。

複数行/複数列で指定したい時は、

df.loc[[1,2], ["Petal.Length", "Species"]]

のようにします。

条件による指定

例えば、Speciesがsetosaである行のみを抜き出したい時、 Rでは、

df[df[,"Species"] == "setosa", ]

のように書きます。 それに対し、pythonでは

df[df["Species"] == "setosa"]

と書きます。また、querymethodを用いて、

df.query("Species == 'setosa'")

とすることも可能です。

querymethodはとても便利で、""の中に、幅広く条件を指定することができます。 例えば、Speciesのcolumnに、'v'という文字を含む箇所だけを取り出したい場合、

df.query("Species.str.contains('v')")

とします。 慣れてくるとquery内の書き方で、様々な条件を指定することができます。 是非マスターしてみましょう。

Series内の要素

DataFrameのアクセス方法と、Seriesがname付きvectorに似ているものという認識が頭に入っていれば、以下も直感的にわかると思います。

行番号による指定

species.iloc[0]

行名による指定

species[1]

もしくは、

species.loc[1]

代入

要素のアクセスの方法が分かっていれば、代入は簡単です。

新しいcolumnを定義しつつ代入

これはRとPythonでほぼ同じです。

df["new_col"] = 0
df.head()
df[,"new_col"] <- 0
head(df)

既存のcolumnに代入

既存のcolumnへの代入は、先ほどとは少々異なります。

行番号/列番号で指定した場所に代入

例えば、new_colの上から5行目までの値を1に変えたい時は、以下のようにします。

df.iloc[:5,5]  = 1
df.head(10)
Sepal.Length Sepal.Width Petal.Length    Petal.Width Species new_col
1   5.1 3.5 1.4 0.2 setosa  1
2   4.9 3.0 1.4 0.2 setosa  1
3   4.7 3.2 1.3 0.2 setosa  1
4   4.6 3.1 1.5 0.2 setosa  1
5   5.0 3.6 1.4 0.2 setosa  1
6   5.4 3.9 1.7 0.4 setosa  0
7   4.6 3.4 1.4 0.3 setosa  0
8   5.0 3.4 1.5 0.2 setosa  0
9   4.4 2.9 1.4 0.2 setosa  0
10  4.9 3.1 1.5 0.1 setosa  0

条件に一致する部分に代入

要素へのアクセス時と同様、条件指定を.loc[]で行い、 =の右辺に代入したい値を書きます。

df.loc[df.Species == 'setosa', "new_col"] = 0
df.head()
Sepal.Length    Sepal.Width Petal.Length    Petal.Width Species new_col
1   5.1 3.5 1.4 0.2 setosa  0
2   4.9 3.0 1.4 0.2 setosa  0
3   4.7 3.2 1.3 0.2 setosa  0
4   4.6 3.1 1.5 0.2 setosa  0
5   5.0 3.6 1.4 0.2 setosa  0

ただし、query methodとの併合はできません。というのも、実はqueryは、条件に一致する新しいDataFrameを作るmethodだからです。

結合

Rのdata.frameとPandasのDataFrameとの間で、 個人的に一番大きいと思われるのが、 結合です。 Pandasを使っていると、改めてRって便利なんだなって思います。

cbind

結合用のDataFrameとして、以下のものを定義します。

df2 = pd.DataFrame([[0]*2]*150, index=[i for i in range(1,151)])
df2.head()
0    1
1   0   0
2   0   0
3   0   0
4   0   0
5   0   0

これを、横方向に結合(Rで言うとcbind)したい場合、

pd.concat([df, df2], axis=1)

とします。 Speciesの右に0と1のcolumnがくっついたはずです。

 Sepal.Length    Sepal.Width Petal.Length    Petal.Width Species 0   1
1   5.1 3.5 1.4 0.2 setosa  0   0
2   4.9 3.0 1.4 0.2 setosa  0   0
3   4.7 3.2 1.3 0.2 setosa  0   0
4   4.6 3.1 1.5 0.2 setosa  0   0
5   5.0 3.6 1.4 0.2 setosa  0   0
... ... ... ... ... ... ... ...
146 6.7 3.0 5.2 2.3 virginica   0   0
147 6.3 2.5 5.0 1.9 virginica   0   0
148 6.5 3.0 5.2 2.0 virginica   0   0
149 6.2 3.4 5.4 2.3 virginica   0   0
150 5.9 3.0 5.1 1.8 virginica   0   0

ただし、index名が異なっていると、こうはいきません。

df2 = pd.DataFrame([[0]*2]*150, index=[i for i in range(0,150)])
pd.concat([df, df2], axis=1)
 Sepal.Length    Sepal.Width Petal.Length    Petal.Width Species 0   1
0   NaN NaN NaN NaN NaN 0.0 0.0
1   5.1 3.5 1.4 0.2 setosa  0.0 0.0
2   4.9 3.0 1.4 0.2 setosa  0.0 0.0
3   4.7 3.2 1.3 0.2 setosa  0.0 0.0
4   4.6 3.1 1.5 0.2 setosa  0.0 0.0
... ... ... ... ... ... ... ...
146 6.7 3.0 5.2 2.3 virginica   0.0 0.0
147 6.3 2.5 5.0 1.9 virginica   0.0 0.0
148 6.5 3.0 5.2 2.0 virginica   0.0 0.0
149 6.2 3.4 5.4 2.3 virginica   0.0 0.0
150 5.9 3.0 5.1 1.8 virginica   NaN NaN

0~150までの151行のindexができます。 片方のDataFrameがもっていない行については、NaNになります。

もし、共通部分を取り出したい時は、join引数に'inner'を指定します(defaultでは'outer')。

pd.concat([df, df2], axis=1, join='inner')
 Sepal.Length    Sepal.Width Petal.Length    Petal.Width Species 0   1
1   5.1 3.5 1.4 0.2 setosa  0   0
2   4.9 3.0 1.4 0.2 setosa  0   0
3   4.7 3.2 1.3 0.2 setosa  0   0
4   4.6 3.1 1.5 0.2 setosa  0   0
5   5.0 3.6 1.4 0.2 setosa  0   0
... ... ... ... ... ... ... ...
145 6.7 3.3 5.7 2.5 virginica   0   0
146 6.7 3.0 5.2 2.3 virginica   0   0
147 6.3 2.5 5.0 1.9 virginica   0   0
148 6.5 3.0 5.2 2.0 virginica   0   0
149 6.2 3.4 5.4 2.3 virginica   0   0

rbind

同様に、縦方向に結合(Rで言うとrbind)したい場合です。 結合用のDataFrameとして、dfdf2を定義し直します。

df = pd.read_csv(path, index_col = 0)
df2 = pd.DataFrame([[1,2,3,4, "hogehoge"],[1,2,3,4, "hogehoge"]], columns=df.columns)
df2.head()
Sepal.Length Sepal.Width Petal.Length    Petal.Width Species
0   1   2   3   4   hogehoge
1   1   2   3   4   hogehoge

dfdf2をrbindの方向にくっつけたい場合、axis=0(default)で行うことができます。

pd.concat([df,df2], axis=0) 
Sepal.Length Sepal.Width Petal.Length    Petal.Width Species
1   5.1 3.5 1.4 0.2 setosa
2   4.9 3.0 1.4 0.2 setosa
3   4.7 3.2 1.3 0.2 setosa
4   4.6 3.1 1.5 0.2 setosa
5   5.0 3.6 1.4 0.2 setosa
... ... ... ... ... ...
148 6.5 3.0 5.2 2.0 virginica
149 6.2 3.4 5.4 2.3 virginica
150 5.9 3.0 5.1 1.8 virginica
0   1.0 2.0 3.0 4.0 hogehoge
1   1.0 2.0 3.0 4.0 hogehoge

最後に

抑えておくべきことをまとめておきます。

  • PandasはRのdata.frameと同様に、テーブル形式のデータを扱えるが、オブジェクト指向であることが大きな違い。
  • RとPythonで似通っている/対応している部分も多い
  • DataFrameはSeriesという名前付きの配列から構成される。
  • Pandasには、Rにはないgroupbyやplotなどの便利なメソッドが満載。
  • DataFrame内の要素にアクセスする時や、結合する時の方法がRと少々異なる。
  • 言語の特性を生かした使い分けができると尚良い。

ご一読ありがとうございました。

【測度論】完全加法族と測度の定義・意義

最近ちょっと測度論をかじっているので、勉強したことの備忘録程度に、測度の定義について書いておこうと思います。

まず、測度を定義する前に、集合族という概念と、完全加法族を導入します。

集合族の導入

Xの集合族\mathscr{E}とは、X部分集合の集まりのことです。 例えば、 X = \{x| x = 1,2,3 \} とします。Xは整数1,2,3からなる集合という意味を持ちます。 この時、Xの集合族\mathscr{E}には、次の様なものが考えられます。

 \mathscr{E} = \{ \{\Phi \},  \{ 1 \}, \{ 2 \}, \{ 3 \},
\{ 1, 2 \}, \{ 1, 3 \}, \{ 2, 3 \}, \{ 1, 2, 3 \}
\}\tag{1}

これは、Xの部分集合を全て含んでいます。 他には、

 \mathscr{E} = \{ \{\Phi\}, \{ 1 \},
\{ 1, 2 \}, \{ 1, 3 \}
\}\tag{2}

の様に、もっと小さい\mathscr{E}も考えられます。

(1)の様に、Xの部分集合の考えられる組み合わせを全て含む場合、   \mathscr{E} Xの部分集合の全体と呼びます。

完全加法族( \sigma-algebra)

定義

さて、完全加法族と言うからには、これも集合族なわけです。 あるXの集合族  \mathscr{T} が、以下の2つを満たす時、   \mathscr{T} 完全加法族であると言います。

( I )    E \in \mathscr{T} ならば、 E^{c}  \in \mathscr{T}

( II ) 
\displaystyle{
 \bigcup_{j=1}^{\infty} E_j \in \mathscr{T} (E_1, E_2, \cdots , \in  \mathscr{T} )
}

( I )は、集合族  \mathscr{T} に入っている全ての集合について、補集合が  \mathscr{T} の要素であることを意味します。 ( II )は、集合族  \mathscr{T} 内の集合のinfinite unionが  \mathscr{T} に入っていることを意味します。

( I )や( II )が成り立たない例を見てみましょう。

例えば、先ほどの例に戻り、   \mathscr{T} =  \{ \{ 1 \},
\{ 1, 2 \}, \{ 1, 3 \}
\} としてみましょう。実は  \mathscr{T} については、 ( I )も( II )も成り立っていません。

( I )については、集合 \{ 1,2 \} の補集合である \{3\}が、   \mathscr{T} に含まれていないことからも明らかでしょう。 また、( II )については、infinite union である、 \{1,2,3\}が、   \mathscr{T} に含まれていません。

しかし、  \mathscr{T} が、集合  Xの全体となっている時は、 ( I )と( II )が成り立ち、  \mathscr{T} は完全加法族となります。

定義から導かれる性質

定義( I )と( II )から、次の様なことが導かれます。

( III )  \displaystyle{
\bigcap_{j=1}^{\infty}  E_j   \in \mathscr{T}
} (E_1,E_2 \cdots, \in \mathscr{T})

これは、ド・モルガンの法則と( I ),( II )より従います。 [tex: Ec]が mathscr{T}に含まれること、それらのinfinite unionが mathscr{T} に含まれることを利用します。 すなわち、


\displaystyle{ \bigcap_{j=1} ^{\infty}  E_j  =  \
\left( \
\bigcup_{j=1}^{ \infty } E_j ^c  \right)^c \
} \in \mathscr{T}

です。

また、次のことも言えます。

( IV )  E,F \in \mathscr{T} \rightarrow E -F \in \mathscr{T}

これは、差集合の定義から、 [tex: \displaystyle{ E -F = E \cap Fc } ]

と表され、  F^c \in \mathscr{T} ( \because ( I ))と、( III ) を用いることで示されます。

また、 \Phi = E -E , X = \Phi^c から、

( V )  \Phi \in \mathscr{T}, X \in  \mathscr{T}

が成り立ちます。 以上、( I ) ~ ( V )は、完全加法族について重要な点です。

測度の定義

測度は、完全加法族とセットで定義されます。 実は測度とは、完全加法族内の集合を実数値に変換する写像なのです。

 \mathscr{T}を、集合 X上の完全加法族とします。 写像  \mu : \mathscr{T} \rightarrow [0, \infty ] が次の2つの条件を満たす時、  \mu  \mathscr{T}上の測度と呼びます。

( i )  \mu(\phi) = 0

( ii ) 互いに交わらない集合列  E_1 , E_2, \cdots, に対して 
\displaystyle{
\mu
\left(
\bigcup_{j=1}^{\infty}
E_j
\right)
= \sum_{j=1}^{\infty}
\mu
\left(
E_j
\right)
}

( i ) が意味するところは、空集合を0に写す写像であること。これは簡単。 ( ii ) が意味するところは、同じ要素を含まない集合ならば、 和集合の測度を、個々の測度の和で表現できるということです。

測度はルベーグ積分の勉強に欠かすことができませんが、 どことなく積分っぽさを感じますね。 リーマン積分ですと、( ii )の左辺の()の中身が、 微小な短冊が作る領域に該当していて、  \mu(\cdot)がその面積を表していると考えていいでしょう。 一方右辺の方は、微小な短冊の面積の和です。

この様に、 測度とは、集合(領域も点の集合である)に面積や体積の様なものを与える写像であることがわかります。

測度空間

集合 X X上の完全加法族 \mathscr{T}、測度 \muを合わせて、測度空間と呼びます。 おまけ程度に。

測度ってなんのためにあるの?

リーマン積分では、ユークリッド空間内の領域の体積を計算していましたが、 測度を定義することで、ユークリッド空間から離れて、一般の集合にも、面積や体積の様なものを定義することができます。

確率密度も、実は測度として定義されるます。 みなさん一度は不思議に思ったことがあるであろう、「ある一点における確率密度が0である」理由も、 測度論を学べば理解できる様になります。

測度論を学ばなくても統計の勉強はできると思いますが、  測度論を学べば確率や統計ともっと仲良くなれるのではないでしょうか。

私もまだ道半ばですので、勉強に戻りたいと思います。ではまた。

歌舞伎町のホストクラブに体験入店して来た ~前編 ~

この度、歌舞伎町のとあるホストクラブの体験入店に行って参りました。結果としてホストになることは断念しましたが、普段得られないような貴重な体験が得られように思います。 また、ホスト業界の闇を垣間見たような気がしたので、体験入店で感じたことを、ブログに書き留めておきます。

新宿の路上で勧誘を受ける

お昼時、新宿を歩いていると、感じの良いおじさん(以降Iさん)に、「お兄さん、ちょっと待って!」と、声をかけられます。いきなり拳を突き合わせて来て、「イェーイ、お兄さんかっこええなあ」と、妙なテンションで絡まれました。

絡み方からして怪しさがぷんぷんしていましたが、普段からかっこいいとは言われ慣れておらず、悪い気分がしなかったので、思わず立ち止まってしまいました。

安っぽいスナップモデルの勧誘か何かかと想像していたのですが、彼の次の言葉は意外にも、 「ホストクラブの人事やっとんのやけど、ホスト興味あらへん?」 でした。

丁度前日に、YoutubeRolandの特集を見ていたということもあり、ホストという職業に若干興味が湧いていたタイミング。その日はちょうど予定もなかったのでとりあえず話だけでも聞くことにしました。

体験1日目

お店で実際に話を聞かせてくれるということで、いざ歌舞伎町の店舗へ!意外に内装が綺麗だし店内のスタッフの人当たりが良くて、好感が持てました。

この日が体験入店の1日目となります。

体験入店とは、実際にホストとして勤務を開始する前に、ホストの仕事が自分に合っているかどうかを判断する為にある期間のことのようです。就活におけるインターンと同様の位置付けでしょうか。

1日目は細かい説明を受けたり先輩社員と話したりします。そして2日目にテーブルマナーについての講習を受けたりら実際に接客をするという感じです。

この日は主に、給与体系や業務内容についての説明を受けました。

給与体系について

お店のテーブルに座り、Iさんに最初に聞かれたのが、「一年後の今、いくら稼ぎたい?」でした。なんとなくその場のノリで、「一千万稼げたらすごいなと思います」と答えたような気がします。

するとIさんは、

「全然夢じゃないし、歌舞伎町では一年目にもっと稼いでるやつなんてザラにいるよ!」

と言います。

夜の業界に詳しくない人は、ホスト=高給取りというイメージが強くあるかと思います。そのイメージうまく利用し、興味を持ってくれた子を引き込もうと言う算段なのでしょう。

しかし、よくよく話を聞いてみると、月商50万に到達していない人たちは、基本給が1日あたり5千円であり、もしお客さんを入れられなかった場合、東京都の最低時給賃金を下回ってしまいます。

売れなかった時のことを考えると、かなり割に合わない仕事です。

歌舞伎町の有名プレイヤーの名前と年収で言葉巧みに新人を引き連れ、その新人が売れれば儲けもの、売れなかったとしても雑用を任せ労働力として安く利用する、という構図が垣間見えたような気がしました。

先輩ホストK

Iさんからの説明が終わると、Iさんは再び、新宿の街に勧誘をしに行きました。

その後、現役新人ホストの人たちと話す時間が設けられました。明るくノリの良いKというホストと、店の掃除やおしぼりの準備をしながら、身の上話をすることに。

元々料理人だったKは、料理を作ること自体より、お客さんに喜んでもらうことに生き甲斐を感じていたそうです。特に、Kは話すことが好きで、お客さんと面と向かって話すことで、自分がお客さんの笑顔に貢献しているという実感を得ていました。

しかし、厨房の仕事が忙しくなり、なかなかお客さんとの会話の時間が持てず、お客さんに貢献しているかどうかが不透明になっていき、生き甲斐が得られなくなっていったそうです。そんな中、僕と同じように路上でホストの勧誘を受け、ホストなら面と向かって誰かを喜ばせられると考え、転職を決意したとのこと。

就活でよくあるような話の流れですが、一回も就活を経験していない上に、僕より3つ年下のKが、自己分析と職業選択を知らず知らずのういに行っている様子に、素直に驚かされました。

Kは掃除の指示もテキパキしており、新人なのに頼り甲斐がありました。身の上話以外の話も楽しく、Kには、ひとりの人間として魅力を感じたように思います。

人事長と面談

朝会を終えるとクラブのVIPルームに連れて行かれ、人事長と1対1で急遽面談をすることに。

元ホストをやっていただけあって、顔もスタイルも良く、スーツもバシッと決まっており、「できるおじさん」感が溢れ出ていました。

数分間簡単に自己紹介をし、何か面白いこと言ってよと言う無茶振りをうまくかわし、適当に話の調子を合わせていると、いつのまにか、話題は恋愛の話に。ここまでルックスもトークも良い印象だったTさんなのですが、恋愛話が吐き気を催すレベルで胸糞でした。

というのもTさんは、奥さんと離婚したばかりだったのですが、その理由が、マタニティブルーが他の人よりひどくて面倒だから、というもの。さはき極め付けはこの一言。

「別れてもあんまショックとはないかなー。別に今も女に困ってる訳でもないしね。」

控えめに言ってクソ男です。お前みたいなやつが女の子を接客してんじゃねえよと、心の底から思いました。元の奥さんと、生まれてくる子供に対して、土下座で謝って欲しい。

こんな男が女性を楽しませ、その対価としてお金を稼ぎ、人の上に立ち、従業員を面談している、、、。そんな組織で僕は働きたくない、端的にそう思いましたね。

体験2日目に向けて

しかしそれでも、Kのように魅力的なホストもいるわけで、彼を含めこの店で売れっ子の人たちが、どのように接客をしているか興味もありました。

そんな訳で、働く気は毛頭ないものの、後日もう一度体験入店に訪れることにしました。

~後編に続く~

大学院生時代に、社会人の友人を持って気づいたこと

f:id:slmnphmet1-2:20200207222822p:plain

僕は、浪人1年+大学生4年+大学院生2年の、合計7年間を「学生」と呼ばれる立場で過ごしてきました。 ストレートで総合大学をでた人と比べて3年間、高卒の人と比べて7年も、余分に学生生活を送ってきたことになります。 以前仲が良かった多くの友人たちは、今では社会でも、すでに新人と呼ばれる立場を卒業し、後輩を育てる側にもなっています。

大学院在学中に、彼らと久しぶりに会って気づくことがありました。 僕もこの春から社会人となってしまう身ですので、 この気づきや価値観が失われてしまう前に、ブログに書き留めておこうと思います。

金銭的価値観が合わなくなる

f:id:slmnphmet1-2:20200207224458p:plain

これがなんと言っても一番大きかったです。 金銭的価値観が変わってしまうと、 何か心理的な距離が生まれてしまった様な気がします。 僕だけでしょうか?

食事

生活に余裕が出たからか、食費にお金を割く人が増えたように思いました。 お互い学生の時は、「軽く牛丼食べて行こうぜ!」みたいな絡みがよく合ったのですが、 この二年間、誰かと牛丼屋に入った記憶がありません。

お金がないから宅飲みで!みたいなことも減りました。 これは、社会人となってしまった彼らが、 週末以外に簡単に泊まりに来れないという理由もあるかとは思います...。 仕方ないっちゃ仕方ないことですが、やはり寂しいなと感じることはありましたね。

旅行

やはり、社会に出て、旅行にお金をかけるようになった友人が多いような気がします。 僕としては、旅行に行く主な目的が気晴らしですので、 車でちょっと遠出できればいいのです(卒業旅行とか、そういう特別な旅行は別です)。 しかし社会人になった友人たちは、金銭的に余裕があるからか、 行ったことのない県、あわよくば海外に行ったりしていました。 あまり遠い場所に誘われると、安いホテルなどを見つけなければ旅行に行けず、 誘ってくれた友人に迷惑をかけてしまった様に思います。

院生=学生の延長=たくさん遊べる!という誤解

f:id:slmnphmet1-2:20200207224556p:plain

院生も大学生なんだから時間があるでしょ?と、 学外の友人に言われたことがあります。 正直に言って、ないです。

授業数は確かに少ないですが、 この時間この授業に出席していれば卒業できるよ〜、 みたいな世界ではありません。 日々の研究の結晶である修士論文が、教授陣に認められ、 卒業に足ると判断された時初めて卒業できます。 実験や解析に時間のかかる研究を行なっている人の中には、 卒業のために沼のように時間を使わなければならない人もいます。

研究時間を割かねばならない、 というだけでなく、それ以外の時間を使って、 交際費や生活費を生み出さなければなりません。

僕をはじめ、研究室や学科の何人かは、夕方まで研究し、 その後バイト、みたいな生活を送っており、 皆忙しそうにしていました。 部活をやっていた方ならわかると思いますが、 院生時代の日常は、中高時代のそれに近いかもしれません。 学業の後に部活、夜クタクタになって帰宅。

しかしさらに辛いのが、一人暮らしによる食事の用意や家事の手間。 帰宅後も疲れた〜ゴロン!みたいなことは、気安くできません。

社会人の友人たちを前にして、 どちらが忙しいと張り合うつもりは毛頭ありません。 ですが、大学院生は研究を仕事とする無賃労働者のようなものだという認識が、 もっと広まって欲しいものです。

ライフイベントが一回り早く訪れる

f:id:slmnphmet1-2:20200207224649j:plain

社会人に比べ、ライフイベントの進展が遅れていることに気づきました。 早ければ早いほど良いわけではないでしょうが、思うところはやはりありますね。

結婚

仲が良かった地元の友人の中には、結婚して子供を育てている人たちがいます。 Facebookからも、同級生同士が結婚した報告が続々と届きます。 一方僕は、25歳にも関わらず、年収100万ちょっと。 到底結婚して子育てなんて考えられません。

ザ・ヤンチャ坊主だった友人も、今では一児の父として落ち着きを見せ、 大人な雰囲気を纏っています。 自分がまだまだ子供のままでいる様な気がしてきました。

転職活動

昨年のことです。「俺実は今。転職活動してるんだ。」と打ちあけれくれた友人がいました。 その当時の僕は、就活を終え、いよいよ来年から就職だ!みたいな時期でしたので、 転職活動という言葉が、とても異質な単語に感じられました。 新卒で入った会社に見切りをつけなければいけないくらい、色々なことを経験したに違いありません。 彼が随分と先のステージにいるような気がしました。

最後に

「俺も学部卒で社会に出ていれば!!」みたいなことを思ったことは、何回もあります。 しかしそんなことは、今考えても全くしょうがないことです。

それに、大学院に入って、修士としての2年間を全うできて、 今では本当に良かったと感じています。 なぜその様に感じたかは、また別の記事でまとめる予定です。

残りの1ヶ月も、悔いのないように過ごせたらなと思います。 ではまた🙇‍♀️

【Python】遊戯王で理解するクラスの継承

~ Pythonを勉強中だけど、クラスがわからない、そんなあなたへ ~

本記事は、以前に書いた記事、 (【Python遊戯王で理解するオブジェクト指向)の第2段に当たります。 slmnphmet1-2.hatenablog.com

インスタンス変数が「カードの基本情報」に、メソッドが「カードの効果」に対応していることは、前回ご紹介したかと思います。 今回は、クラスを継承するとは何か、ということを、またも遊戯王を引き合いに出しながら確認していきます。

Pythonを勉強中だけどクラスってよくわからないんだよね〜」という人、 もしくは、クラスはわかってきたんだけど、継承の意味やメリットがわからない!という人が、 少しでもイメージをつかんでくれたら嬉しいです。

継承とは、 カードの種類を指定すること

継承とは、親クラス(カードの種類)の情報を、子クラス(個々のカード)に渡すことだと思うとわかりやすいでしょう。 今回は、強欲な壺などの魔法カードを例に考えていきたいと思います。

「魔法カード」をクラスで表現してみる

遊戯王において、魔法カードは主に手札から発動し、効果の適用が終わったら墓地に置かれます(永続魔法や装備魔法などは一旦考えないことにします)。 pythonの言葉で書くと、以下のようにすれば良いでしょう。

class Magic :
    def __init__(self) :
        self.cardtype = "魔法カード"
        self.name = None

    def activate(self) :
        hands.remove(self.name)
        self.effect()
        cemetary.append(self.name)

    def effect(self) :
        pass

Magicには、メソッドが二つ定義してあります。 activate(発動)では、手札(hands というリスト)から自分自身(self.name)を取り除き、 効果を適用(self.effect)し終えたら墓地(cemetary)に置くという意味です。

インスタンス変数のself.name とメソッドのeffectには、現状何も書かれていません。 なぜならば、定義したい魔法カードごとに名前や効果が異なるためです。 逆に言えば、self.nameとeffectさえ定義すれば、(activateをいちいち書き直さなくても)好きな魔法カードが定義できることになります。 以下で具体的に見てみましょう。

魔法カードのルールを継承させてみる

例として、魔法カードクラス(Magic)を継承し、強欲な壺(英名 : Greedy Pot)というクラスを定義してみることにします。

遊戯王 強欲な壺 107-044 ノーマル 【ランクA】 【中古】

class GreedyPot(Magic) :
    def __init__(self) :
        self.name = "強欲な壺"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

        hands.append(decks[0])
        decks.remove(decks[0])

見ての通り、強欲な壺(GreedyPot)というクラスには、発動(activate)というメソッドが書かれていません。 しかし、魔法カードであることを指定(Magic クラスを継承)しているので、発動(activate)の処理を行うことができます。 activateの中ではeffectを行う(効果を適用する)処理があります。強欲なツボの効果(effect)は2枚ドローですので、 デッキ(配列 decks)から、カード2枚を取り出し、手札(配列 hands)にその二枚を加えます。

それでは、実際に強欲な壺というカードを発動(card.activate())してみましょう。 デッキにカードが2枚、手札に強欲なツボがあるとき、activateの後にどうなるかというと...

decks = ["グレムリン", "ブラック・マジシャン"]
hands = ["強欲な壺"]
cemetary = []

card = GreedyPot()
card.activate()

print(f"手札 : {hands}")
print(f"デッキ : {decks}")
print(f"墓地 : {cemetary}")
手札 : ['グレムリン', 'ブラック・マジシャン']
デッキ : []
墓地 : ['強欲な壺']

手札が2枚増えて、墓地に強欲な壺が置かれるはずです。

同じように、成金ゴブリン(英名 : Upstart Goblin)というカードを定義してみましょう。 (ライフ回復効果は、今回省きます。)

遊戯王 RC02-JP043 スーパーレア 魔法 成金ゴブリン 【中古】【Sランク】

class UpstartGoblin(Magic) :
    def __init__(self) :
        self.name = "成金ゴブリン"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

やはり、逐一activateを書かなくて良いので、手間が省ける上、コードが見やすくなりますね。

decks = ["グレムリン", "ブラック・マジシャン"]
hands = ["成金ゴブリン"]
cemetary = []

card = UpstartGoblin()
card.activate()

print(f"手札 : {hands}")
print(f"デッキ : {decks}")
print(f"墓地 : {cemetary}")

ちなみに発動後はこんな感じです。

手札 : ['グレムリン']
デッキ : ['ブラック・マジシャン']
墓地 : ['成金ゴブリン']

このように、親クラス(Magic)を継承することで、親と共通する処理(activate)を省き、 コードが冗長になるのを防ぐことができます。Magicクラスを継承したときに、 魔法カードとしてのルールが適用されるんだということが、 コードを見る人にも一目瞭然ですね。

super()により、「魔法カード」としての基本情報を継承

しかしこのままでは、継承しきれていないものがあります。それは、親クラス(Magic)のインスタンス変数です。

親クラスのMagicには、インスタンス変数として、cardtypeが定義されていました。これを呼び出せば、「魔法カード」と出力されます。

magic = Magic()
print(magic.cardtype)

しかし、継承後に新しくコンストラクタを定義すると、この情報が失われます。 以下はエラーになります。

card = GreedyPot()
print(card.cardtype)

強欲な壺は魔法カードですから、cardtypeにアクセスしたときに、「魔法カード」と出力されて欲しいですよね。 そこで、super()の出番です。

class GreedyPot(Magic) :
    def __init__(self) :
        super().__init__()
        self.name = "強欲なツボ"

    def effect(self) :
        hands.append(decks[0])
        decks.remove(decks[0])

        hands.append(decks[0])
        decks.remove(decks[0])

card = GreedyPot()
print(card.cardtype)

今度は正しく「魔法カード」と出力されるはずです。 def init(self)直下に、super().init()を置いてやれば、 新しくコンストラクタを定義したときでも、 親クラス(Magic)のカードのインスタンス変数(カードの基本情報) を引き継ぐことができます。

まとめ

クラスの継承とは、カードの種類を指定し、そのルールを子クラス(個々のカード)に適用させることと対応しています。 また、super()は、継承後にも親クラスの情報を引き継ぐのに用いられます。 クラスの継承によって、冗長なコードが回避でき、複数の子クラス(個々のカード)を定義するときに見やすくなりますね。

遊戯王に例えると、イメージを掴みやすくなる分、正確さを欠きます。概観が掴めたという人は、是非オブジェクト指向の厳密な理解に励んでください。

最後に、参考になるサイトのリンクを貼っておきます。一読いただき、ありがとうございました。

python-works.hatenablog.com memopy.hatenadiary.jp qiita.com

【Python】遊戯王で理解するオブジェクト指向

~ Pythonを勉強中だけど、クラスがわからない、そんなあなたへ ~

お久しぶりです。2020年4月から、遊戯王の新アニメ、「遊戯王SEVENS」が始まろうとしています。 ラッシュデュエルという全く新しい遊び方が開発中であり、 時代は変わっていくんだなあ...と物思いにふけっている今日この頃です。

さて、本記事は、「Pythonを勉強中だけどclassってよくわからないんだよね〜」という方々が、 遊戯王の知識によりclassの概念を補完し、オブジェクト指向を理解する手助けになればいいな、という思いを込めて書きました。 一度classの勉強をして挫折したデュエルモンスターズ世代の同志が、 遊戯王の思い出とプログラミングへの自信を取り戻してくれたら嬉しいです。

また、プログラミングはわからないけど遊戯王は好きという人も、興味本位で覗いていってもらえると嬉しいです。 もしくは、プログラミング勉強中の遊戯王好きなお友達に宣伝していただけると励みになります。 ※ 遊戯王に例えると、イメージを掴みやすくなる分、正確さを欠きます。概観が掴めたという人は、是非オブジェクト指向の厳密な理解に励んでください。

クラス :「カードの基本情報」

クラスとは、遊戯王で例えるならば、カードの基本情報が書かれたものです。例えば、下のクラス 「BlackMagicianGirl」は、遊戯王カード「ブラック・マジシャン・ガール」の情報を表現したものです。

遊戯王  ブラック・マジシャン・ガール(U)(15AY-JPA04)

class BlackMagicianGirl :
    def __init__(self) :
        self.atack = 2000
        self.defence = 1700
        self.attribute = "闇"
        self.tribe = "魔法使い"
        self.name = "ブラック・マジシャン・ガール"
        self.level = 6

    def effect(self) :
        n_master = sum(["ブラック・マジシャン" in k for k in cemetary])
        self.atack += 300*n_master

カード情報は、大きく分けて、カードのステータスである「インスタンス変数」と、 カードの効果である「メソッド」に分かれます。 2行目、def init(self) 以下が、インスタンス変数にあたり、 攻撃力や守備力、属性、種族、レベルなどが書かれています。 11行目、def effect(self) 以下がメソッドにあたり、 ブラック・マジシャン・ガールの効果を表現しています。

メソッドを持たないクラス : 通常モンスター

メソッドを持たないインスタンス変数のみクラスももちろん定義できます。 遊戯王で例えるならば、効果を持たない通常モンスターにあたります。 例えばグレムリンだとこんな感じです。

【中古】[TCG]遊戯王 15AY-JPA11N グレムリン(20140308)

class Gremlin :
    def __init__(self) :
        self.atack = 1300
        self.defence = 1400
        self.attribute = "闇"
        self.tribe = "悪魔"
        self.name = "グレムリン"
        self.level = 4

インスタンス変数を持たないクラス : 魔法カード

メソッドを持たないクラスが通常モンスターであったように、 インスタンス変数を持たないメソッドのみのクラスを定義できます。 これは、攻撃力は守備力等のステータスを持たない、いわば魔法カードと言っていいでしょう。

小学生の頃めちゃくちゃお世話になったカード、「治療の神 ディアン・ケト」をクラスで書いてみます。

【プレイ用】遊戯王 YSD3-JP023 治療の神 ディアン・ケト(日本語版 ノーマル) 【中古】

class DianKeto :
    
    def activate(self) :
        new_life = life + 1000
        return(new_life)

ディアン・ケトライフポイントを1000回復してみましょう。

life = 4000
print(f"ライフポイント : {life}")
keto = DianKeto()
life = keto.activate()
print(f"ライフポイント : {life}")

出力はこんな感じです。

ライフポイント : 4000
ライフポイント : 5000

ライフポイントが4000から5000に上がります。 言わずと知れた最強カードです(大嘘)。

インスタンス化 : カードを「場に出すこと」

インスタンス化とは、クラスを呼び出すことです。 一度呼び出したカードの情報は、以下のように確認することができます。

BMG = BlackMagicianGirl()
print(f"カード名 : {BMG.name}")
print(f"属性 : {BMG.attribute}")
print(f"種族 : {BMG.tribe}")
print(f"レベル : {BMG.level}")

以下は出力です。

カード名 : ブラック・マジシャン・ガール
属性 : 闇
種族 : 魔法使い
レベル : 6

またインスタンス化とは、カードの召喚に近いでしょう。 一度呼び出した(召喚した)カードの情報は書き換えることが可能です。 それでは、ブラック・マジシャン・ガールを召喚(インスタンス化)し、自身の効果(メソッド)を適用し、 攻撃力を上げて見ましょう。 ブラック・マジシャン・ガールの攻撃力は、cemetery(墓地)にあるブラック・マジシャンの数につき、300上がります。 例えば、墓地にグレムリン1体とブラック・マジシャン1体の時。

cemetary = ["グレムリン", "ブラック・マジシャン"]
BMG = BlackMagicianGirl()
print(f"攻撃力 : {BMG.atack}")
BMG.effect()
print(f"攻撃力 : {BMG.atack}")

出力は以下のようになります。

攻撃力 : 2000
攻撃力 : 2300

場にいるBMG(ブラック・マジシャン・ガール)の攻撃力が、効果の適用後300上がりましたね。

一方ここで、

print(BlackMagicianGirl().atack)

としてみると、攻撃力は2000のままです。 しかしこれは先ほどインスタンス化(召喚)したBMGではないので、当たり前です。 クラスに書かれていることはカードの基本情報な訳ですから、簡単に書き換わってしまっては困ります。

つまり、インスタンス化した(場に出た)後、インスタンス(場のカード)の値は変化しても、 クラスそのものの値(カードの基本情報)は書き変わらないのです。 ここがオブジェクト指向の一つの肝なのではないでしょうか。

引数を持つメソッド : 対象を取る効果

メソッドに引数を与えることも可能です。厳密には違うのでしょうが、 「引数を持つメソッド」は「対象を取る効果」に近いかもしれません。 例えば、「突進」を以下のように実装してみます。

遊戯王カード 突進 レア ビギナーズ・エディション Vol.1 7期 BE01 YuGiOh! | 遊戯王 カード レア 速攻魔法

class Tosshinn :
    def __init__(self) :
        self.card_type = "magic"

    def activate(self, target) :
        target.atack += 700

突進で、場のブラック・マジシャン・ガールを対象にとり、攻撃力を上げてみましょう。

BMG = BlackMagicianGirl()
print(f"攻撃力 : {BMG.atack}")
Tosshinn().activate(BMG)
print(f"攻撃力 : {BMG.atack}")

出力はこんな感じです。

攻撃力 : 2000
攻撃力 : 2700

突進をactivate()発動した前と後で、BMGの攻撃力が変化していますね。

クラス外の変数を書き換える

ディアン・ケトの実装コードであれ?と思った方もいるかもしれません。 というのも、new_life という変数を定義せずに、

class DianKeto :

    def activate(self) :
        life += 1000
        return(life)

life = 4000
print(f"ライフポイント : {life}")
keto = DianKeto()
life = keto.activate()
print(f"ライフポイント : {life}")

とした方がシンプルなのです。しかしこれは、変数スコープの都合でエラーになるので注意が必要です。 (https://qiita.com/N-qiita/items/de972c176f50c22414a6)

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-68-ef833056dfe9> in <module>
----> 1 life = keto.activate()

<ipython-input-64-b3c8af5464db> in activate(self)
      2 
      3     def activate(self) :
----> 4         life += 1000
      5         return(life)

UnboundLocalError: local variable 'life' referenced before assignment

ライフポイントや墓地などの、 どのカードも用いる可能性のある変数は、 外部変数として定義しておいて、メソッド内で外部変数だということを教えてやればいいのです。

class DianKeto :

    def activate(self) :
        global life
        life += 1000
        return(life)

life = 4000
print(f"ライフポイント : {life}")
keto = DianKeto()
life = keto.activate()
print(f"ライフポイント : {life}")
ライフポイント : 4000
ライフポイント : 5000

次回の予定

次回は、クラスの継承やオーバーライドを、同じく遊戯王で例えて説明していきたいと思います。 拡散いただけると励みになります。おもろいなとかちょっと役立つなと感じた方は、 デュエリストのお友達に教えてあげてください。 ではまた!