nykergoto’s blog

機械学習とpythonをメインに

ゼロ状態を考える

今回はコードを書く時の気持の話です.

たとえば配列 x の要素が 0.5以下の掛け算の値を取得する、みたいなことをしたいとします. たとえば以下の code で出来るような処理です (map つかおうやとかは置いておいて...)

import numpy as np

def calc_factor(x):
  if x > .5:
    return None

  return x

X = np.random.uniform(size=10)
retval = None

for x_i in X:
  # 要素に対して掛け算の値を計算
  r_i = calc_factor(x_i)

  # 条件に合わない時何もしない
  if r_i is None:
    continue
  
  # 今までに値がなかったら今計算した値で置き換え
  if retval is None:
    retval = r_i
  
  # そうでない時掛け算して更新する
  else:
    retval *= r_i

このコードを書いた人は大変真面目なので値が全く存在しないゼロ状態のことを None と表現していることがわかります. それは

  • calc_factor で条件にマッチしない時 None を返していること
  • 計算結果 retval の初期値が None であること

からわかりますね. ここで掛け算という計算(演算)を考えると 1 を書けても結果は変わりません. これは足し算でゼロを足してもゼロになるのと同じです. これを使うと上記は以下のようになるでしょう.

import numpy as np

def calc_factor(x):
  if x > .5:
    # None を返さない
    return 1

  return x

X = np.random.uniform(size=10)
# None で初期値としない
retval = 1

for x_i in X:
  # 全部掛け算していく
  retval *= calc_factor(x_i)

だいぶスッキリ書けましたね! この掛け算という計算に対して何も変わらないものを考えてきれいにする、という操作は、配列の足し算 (concat) とか Object の要素の追加とか、他の概念の演算に対しても言えることです。

配列の追加

たとえば配列の要素から新しい配列を作成して全部まとめるみたいな処理があったとしましょう. map とかを使わないとすると以下のような感じ

def create_array(value) -> Optional[List]:
  if value > 5:
    return None
  
  return [value] * value


new_array = []

for x in range(10):
  value_i = create_array(x)

  if value_i is None:
    continue
  new_array.extend(value_i)

配列の足し算に対して空っぽの配列 [] はゼロの意味を持っています。のでこれを使うと以下のようになります. すっきり!

def create_array(value) -> List:
  if value > 5:
    # なにもないとき None を返さない
    return []
  
  return [value] * value

# None で初期値としない
new_array = []

for x in range(10):
  # 全部足し算していく
  new_array += create_array(x)

pandas.DataFrame の場合

たとえばですが pandas.DataFrame などもこれと同等の操作をできるようにAPIを設計してくれています. 例えば配列の値が 0.5 以下の場合にランダムにデータフレームを作成、横方向につなげる、ということをやりたいとしましょう (そんなん意味あるんかというのは置いておいて、機械学習の何かしらでありそうな感じになってきました.

import pandas as pd

def create_df(x):
  if x > .5:
    return None

  return pd.DataFrame(np.random.uniform(size=(10, 4)))

out_df = None
x = [.3, .4, .6, .2, .3]

for x_i in x:
  _df = create_df(x_i)

  # 値がない時なにもしない
  if _df is None:
    continue

  # 今までに計算している値が無かったら置き換え
  if out_df is None:
    out_df = _df
  # そうでなければ横に足し算する
  else:
    out_df = pd.concat([out_df, _df], axis=1)

実は pd.concat という操作に対して pd.DataFrame() はゼロを意味します(pd.DataFrame() を concat しても結果が変わらない). これを使うと、さきほどの数値に対する掛け算や、配列に対する足し算とおんなじようなことができます.

import pandas as pd

def create_df(x):
  if x > .5:
    # zero 状態の data frame をかえす
    return pd.DataFrame()

  return pd.DataFrame(np.random.uniform(size=(10, 4)))

# zero 状態の data frame を初期値とする
out_df = pd.DataFrame()
x = [.3, .4, .6, .2, .3]

for x_i in x:
  # 全部くっつける
  out_df = pd.concat([out_df, create_df(x_i)], axis=1)    

Django Q object の場合

Django というフレームワークの話です。Djangoではデータベースへのアクセスで、データを絞り込むクエリーを表現する Q instance というものがあります。

絞り込みは複数の and とか or をやりたいので、 Q にたいしても and / or を行うことで複数条件での絞り込みを表現できるように Django が設計してくれています。 例えば Q(hoge=1) & Q(huge=10) とやると hoge=1 かつ huga=10 のデータを検索してくれます。

じつは Q のAPIでは and /or の操作に対して Q() 渡しても結果は変わりません(!) これは先ほどから見ていた「掛け算で1を掛けても結果がかわらない」という関係性と同じですね!

ですから、例えば複数の条件を or でまとめたいな−というとき、以下のようなコードは正しいのですが、

def calc_query_object(value):
  if value > 5:
    return None
  # 何らかの大変な条件の計算によって kwrgs が出来るとする
  kwrgs = {}
  return Q(**kwrgs)

q_object = None

# なんかたくさんの条件を or 条件でまとめたい
for value in range(10):
  q_i = calc_query_object(value)

  if q_i is None:
    continue

  if q_object is None:
    q_object = q_i
  else:
    # or でまとめる
    q_object = q_object | q_i

ゼロ状態を意識して

def calc_query_object(value):
  if value > 5:
    # 空っぽの Q を返す
    return Q()
  kwrgs = {}
  return Q(**kwrgs)

# 空っぽの Q で初期化する
q_object = Q()

for value in range(10):
  # 全部ガッチャンコ
  q_object = q_object | calc_query_object(value)

と記述したくなってきますね;)

面白いなと思ったら

モナドという概念を調べてみてください. ここで紹介したのはモナドの一部です.