しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

カイ二乗検定とは何かを調べてみる

統計について調べていたら「カイ二乗検定」という単語が出てきたので調査。

カイ二乗検定とは

カイ二乗検定 - Wikipedia

帰無仮説が正しければ検定統計量が漸近的にカイ二乗分布に従うような統計学的検定法の総称である。次のようなものを含む。

Wikipediaによるとカイ二乗検定とは上のような定義になります。
同じくWikipediaによると、以下のような統計学的検定法の総称のようです。

定義を読むだけでは難しいので、実際の問題を解きながら勉強してみようと思います。

カイ二乗検定の具体例

カイ二乗検定 - 大人になってからの再学習

上のサイトを参考に勉強しました。
こちらのサイトの例2(サイコロを複数回振った結果を元に、サイコロが歪んでいると言えるか求める)の手順を見たところ、カイ二乗検定は下の手順で正しさを検証するようです。

  1. 期待する値(期待度数)、上の例だとサイコロのそれぞれの値が出る確率を算出する
  2. (“実際にある値が出た数” - 期待度数)の2乗 / 期待度数を算出する
  3. それぞれの値について、2で出した値を足し合わせる(この結果をカイ二乗値と呼ぶ)
  4. 今回の問題に関する自由度を求める(今回は項目数6なので6-1で5になる)
  5. 今回の問題に関する有意水準を決める(今回は5%)
  6. 自由度、カイ二乗値、有意水準カイ二乗分布表を使ってサイコロが歪んでいるかどうかの判断をする

上の手順を参考に、実際に自分でも問題を解いてみようと思います。
以下のような問題を定義してみました。

問題

あるグループの血液型分布は下のようなものになった。

血液型 人数
A型 18人
B型 13人
0型 14人
AB型 5人

これは日本全体の血液型分布と異なると言えるか。
日本全体の血液型分布は「A型40%、B型20%、O型30%、AB型10%」と仮定、有意水準は5%とする。

回答

まずは期待度数を算出します。
日本全体の分布は「A型40%、B型20%、O型30%、AB型10%」なので期待度数は以下のようになります。

血液型 人数
A型 20人
B型 10人
0型 15人
AB型 5人

それぞれの血液型の (観測結果 - 期待度数)の2乗 / 期待度数 は以下のようになります。

血液型 人数
A型 0.2
B型 0.9
0型 0.67
AB型 0

カイ二乗値はそれらを足した数なので、1.77になります。
そして自由度は4-1で3となります。

カイ二乗分布表によると、自由度3で5%の確率の値は7.81になります。
今回算出したカイ二乗値はそれより低いので、このグループの血液型分布は日本全体の血液型分布と同じだと言えそうです。

ChameleonでUIColorを便利に使う

Chameleonというライブラリを試してみました。

github.com

インストー

CocoaPodsでインストールします。

pod "ChameleonFramework"

使い方

UIColorに多くのフラットカラーが追加されます。

import UIKit
import ChameleonFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        [
            UIColor.flatRed()!,
            UIColor.flatLime()!,
            UIColor.flatPlum()!
            ].enumerated().forEach {
                
                let colorView = UIView(frame: CGRect(x: 100, y: 100 + 30 * $0.offset, width: 100, height: 30))
                colorView.backgroundColor = $0.element
                view.addSubview(colorView)
        }
    }
}

色は以下のようなものです。

f:id:llcc:20170626233309p:plain

色の明度変更をする事もできます。

import UIKit
import ChameleonFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let darkView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 30))
        darkView.backgroundColor = UIColor.red.darken(byPercentage: 0.5)
        view.addSubview(darkView)
        
        let lightView = UIView(frame: CGRect(x: 100, y: 130, width: 100, height: 30))
        lightView.backgroundColor = UIColor.red.lighten(byPercentage: 0.5)
        view.addSubview(lightView)
    }
}

下のように明るい赤と暗い赤を取得できます。

f:id:llcc:20170626233718p:plain

グラデーションも簡単に実装できます。

import UIKit
import ChameleonFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let colorView = UIView(frame: CGRect(x: 100, y: 130, width: 100, height: 30))
        colorView.backgroundColor = UIColor(gradientStyle: .leftToRight, withFrame: CGRect(x: 100, y: 100, width: 100, height: 30), andColors: [UIColor.red, UIColor.blue, UIColor.purple])
        view.addSubview(colorView)
    }
}

以下のように赤、青、紫のグラデーションを表現できました。

f:id:llcc:20170626233958p:plain

complementaryFlatColorOfを使うとフラットな補色を取得する事ができます。

import UIKit
import ChameleonFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let colorView = UIView(frame: CGRect(x: 100, y: 130, width: 100, height: 30))
        colorView.backgroundColor = UIColor(complementaryFlatColorOf: UIColor.red)
        view.addSubview(colorView)
    }
}

引数に赤を渡したら、下のようにフラットな緑色が返ってきました。

f:id:llcc:20170626234546p:plain

色に合わせて白 or 黒を返してくれるメソッドもあります。
黄色だったら黒・赤だったら白など、その色と対照的な色を返してくれます。
色の上に乗せる文字色を決めるのに便利そうです。

import UIKit
import ChameleonFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let colorView = UIView(frame: CGRect(x: 100, y: 130, width: 100, height: 30))
        colorView.backgroundColor = UIColor(contrastingBlackOrWhiteColorOn: UIColor.yellow, isFlat: true)
        view.addSubview(colorView)
    }
}

他にも画像の特定座標の色取得など、便利な機能が揃っているようです。

ダイクストラ法で最短経路を求めてみる

ダイクストラ法という最短経路を求めるアルゴリズムをSwiftで試してみました。
参考にさせて頂いたのはこちらの記事です。

ダイクストラ法(最短経路問題)

ダイクストラ法とは

ダイクストラ法(だいくすとらほう、英: Dijkstra’s algorithm)はグラフ理論における辺の重みが非負数の場合の単一始点最短経路問題を解くための最良優先探索によるアルゴリズムである。

Wikipediaによると上記のようなものになります。

ダイクストラ法 - Wikipedia

今回はこれをSwiftで実装してみます。

実装

まずは画面上に点を設置します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        (0...5).forEach { _ in
            let dot = UIView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            view.addSubview(dot)
        }
    }
}

f:id:llcc:20170519000635p:plain

次にStartとGoalを決めます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [UIView] = []
        (0...5).forEach { _ in
            let dot = UIView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
    }
}

f:id:llcc:20170519002227p:plain

続けて線同士を繋げて経路を作ろうと思います。

各点に他経路への参照を持たせるためにDotViewというクラスを作って各点をUIViewからDotViewにします。
下だと場合によっては循環参照が起こったり場合によってゴールまで着けない事もあるのですが、今回は「ダイクストラ法」の勉強という事で無視します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = [] // UIViewをDotViewに変更
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect( // UIViewをDotViewに変更
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        
        // 以下今回追加
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
    }
}

// 以下が今回追加分
class DotView: UIView {
    var otherDots: [UIView] = []
}

上で参照を持たせたので、次は線を引きます。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        // 以下は今回追加
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
    }
}

class DotView: UIView {
    var otherDots: [DotView] = []
}

// 以下は今回追加
class LineView: UIView {
    let start: CGPoint
    let end: CGPoint
    
    init(start: CGPoint, end: CGPoint) {
        self.start = start
        self.end   = end
        
        super.init(frame: CGRect(
            x: ([start.x, end.x].min() ?? 0) + 10,
            y: ([start.y, end.y].min() ?? 0) + 10,
            width: abs(start.x - end.x),
            height: abs(start.y - end.y)))
        
        backgroundColor = UIColor.clear
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: start.x - frame.origin.x + 10, y: start.y - frame.origin.y + 10))
        path.addLine(to: CGPoint(x: end.x - frame.origin.x + 10, y: end.y - frame.origin.y + 10))
        path.lineWidth = 4.0
        UIColor.darkGray.setStroke()
        path.stroke()
    }
}

これで経路を作る事ができました。

f:id:llcc:20170520130216p:plain

ここからダイクストラ法を実装していきます。
まずはDotViewにtypeとscore(スタートからの距離)を追加して、viewDidLoadの中でtypeを設定します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        dots.first?.type = .start // 追加
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        dots.last?.type = .goal // 追加
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
    }
}

// 今回修正
class DotView: UIView {
    enum DotType {
        case start
        case goal
        case normal
    }
    
    var otherDots: [DotView] = []
    var score: CGFloat = 0
    var type: DotType = .normal
    var routeDots: [DotView] = []
}

// 省略

経路を作ったので、どれが最短経路かを計算します。
ViewControllerを以下のように修正します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        dots.first?.type = .start
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        dots.last?.type = .goal
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
        
        if let start = dots.first {
            var targetDots = [start]
            while targetDots.count != 0 {
                let targetDot = targetDots.remove(at: 0)
                targetDot.otherDots.forEach {
                    let dx = targetDot.frame.origin.x - $0.frame.origin.x
                    let dy = targetDot.frame.origin.y - $0.frame.origin.y
                    if $0.score == 0 || targetDot.score + sqrt(dx*dx + dy*dy) < $0.score {
                        $0.score = targetDot.score + sqrt(dx*dx + dy*dy)
                        targetDots.append($0)
                        $0.routeDots = targetDot.routeDots + [targetDot]
                    }
                }
            }
        }
        if let last = dots.last, last.routeDots.count != 0 {
            last.routeDots.append(last)
            (0...(last.routeDots.count - 2)).forEach {
                let lineView = LineView(
                    start: last.routeDots[$0].frame.origin,
                    end: last.routeDots[$0 + 1].frame.origin,
                    color: UIColor.red,
                    lineWidth: 2)
                view.addSubview(lineView)
            }
        }
    }
}

class DotView: UIView {
    enum DotType {
        case start
        case goal
        case normal
    }
    
    var otherDots: [DotView] = []
    var score: CGFloat = 0
    var type: DotType = .normal
    var routeDots: [DotView] = []
}

class LineView: UIView {
    let start: CGPoint
    let end: CGPoint
    let color: UIColor
    let lineWidth: CGFloat
    
    init(start: CGPoint, end: CGPoint, color: UIColor = UIColor.darkGray, lineWidth: CGFloat = 4) {
        self.start = start
        self.end   = end
        self.color = color
        self.lineWidth = lineWidth
        
        super.init(frame: CGRect(
            x: ([start.x, end.x].min() ?? 0) + 10,
            y: ([start.y, end.y].min() ?? 0) + 10,
            width: abs(start.x - end.x),
            height: abs(start.y - end.y)))
        
        backgroundColor = UIColor.clear
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.lineWidth = lineWidth
        color.set()
        path.move(to: CGPoint(x: start.x - frame.origin.x + 10, y: start.y - frame.origin.y + 10))
        path.addLine(to: CGPoint(x: end.x - frame.origin.x + 10, y: end.y - frame.origin.y + 10))
        path.stroke()
    }
}

実際の計算は下の部分です。
スタートから順番に点を辿り、その順路が最短なら更に探索、最短でなければそのルートは探索終了しています。

if let start = dots.first {
    var targetDots = [start]
    while targetDots.count != 0 {
        let targetDot = targetDots.remove(at: 0)
        targetDot.otherDots.forEach {
            let dx = targetDot.frame.origin.x - $0.frame.origin.x
            let dy = targetDot.frame.origin.y - $0.frame.origin.y
            if $0.score == 0 || targetDot.score + sqrt(dx*dx + dy*dy) < $0.score {
                $0.score = targetDot.score + sqrt(dx*dx + dy*dy)
                targetDots.append($0)
                $0.routeDots = targetDot.routeDots + [targetDot]
            }
        }
    }
}

これを実行すると、以下のように最短経路を表示できた事が分かります。

f:id:llcc:20170520191211p:plain

rspec-retryでfeature specを安定させる

rspec-retryという、失敗したテストを再実行するGemを使ってみました。

GitHub - NoRedInk/rspec-retry: retry randomly failing rspec example

まずはRailsのプロジェクトを作ってfeature specを書きます。

require 'rails_helper'

feature 'test' do
  scenario 'ページが表示される' do
    visit users_path

    expect(current_path).to eq users_path
  end
end

ここに一回目だけ失敗する処理を追加します。

require 'rails_helper'

i = 0
feature 'test' do
  scenario 'ページが表示される' do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

これを実行するとエラーになります。

f:id:llcc:20170510000358p:plain

次はrspec-retryでエラーが出たら何回かリトライするようにします。
まずはGemfileにrspec-retryを追加します。

group :development, :test do
  gem 'rspec-retry'
end

次はspec_helper.rbにrspec-retry関連の設定を追加します。

require 'rspec/retry'

RSpec.configure do |config|
  config.verbose_retry = true
  config.display_try_failure_messages = true
end

最後にscenarioにretry回数を追加します。

require 'rails_helper'

i = 0
feature 'test' do
  scenario 'ページが表示される', retry: 3 do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

実行すると、1回目で失敗したけど2回目で成功したことが分かります。

f:id:llcc:20170510000835p:plain

retryはscenarioではなくfeatureに付ける事もできます。

require 'rails_helper'

i = 0
feature 'test', retry: 3 do
  scenario 'ページが表示される' do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

feature spec以外で使う事もできます。

require 'rails_helper'

i = 0
RSpec.describe User, type: :model, retry: 3 do
  it do
    i += 1
    raise if i == 1
    expect(1).to eq 1
  end
end

f:id:llcc:20170510001152p:plain

全feature specを対象にしたい場合は、spec_helper.rbに以下に設定を追加します。

require 'rspec/retry'

RSpec.configure do |config|
  config.verbose_retry = true
  config.display_try_failure_messages = true
  config.around :each, type: :feature do |ex|
    ex.run_with_retry retry: 3
  end
end

モンテカルロ法で円周率を求める

囲碁AIで有名なモンテカルロ法で円周率を計算できるようなので試してみました。

モンテカルロ法とは、Wikipediaによるとシミュレーションや数値計算を乱数を用いて行う手法の総称とのことです。

モンテカルロ法 - Wikipedia

実装方法

計算の手順は下の通りです。

  1. 四角形とそれに内接する円を作成する
  2. 四角形の中に多数の点を配置する
  3. 円の内部にある点の数を円の面積だと仮定して円周率を計算する

まずは四角形と内接する円を作成します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
    }
}

これを実行すると、四角形と円が描画されます。

f:id:llcc:20170508235355p:plain

次は四角形の上に点を配置していきます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        // ここから今回追加
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
    }
}

実行すると、点が配置されているのが分かります。

f:id:llcc:20170509000427p:plain

次に円の中の点の数を計算します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
        
        // ここから今回追加
        let count = dots.filter {
            let dx = $0.frame.origin.x - circle.center.x
            let dy = $0.frame.origin.y - circle.center.y
            let radius = circle.frame.size.width / 2
            return dx*dx + dy*dy < radius*radius
        }.count
        print(count) // → 74
    }
}

最後に円の中の点(円の面積)を元に円周率を求めます。
結果は、3.1111と3.14に近い数字となりました。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
        
        let count = dots.filter {
            let dx = $0.frame.origin.x - circle.center.x
            let dy = $0.frame.origin.y - circle.center.y
            let radius = circle.frame.size.width / 2
            return dx*dx + dy*dy < radius*radius
        }.count
        
        let radius = circle.frame.size.width / 2
        print(CGFloat(count) / radius / radius) // 3.1111
    }
}

【GameplayKit】GKRuleSystemで複雑な条件を管理する

GKRuleSystemという複数の条件を管理する機能を使ってみました。

使い方は下の通りです。

  1. GKRuleSystemにGKRuleを追加する
  2. GKRuleSystemにパラメータをセットする
  3. GKRuleSystemの評価メソッド(evaluate)を呼ぶ
  4. 評価の結果を取得する

コードは下の通りです。
評価の結果は、GKRuleの引数のgradeの数字が返ってきます。
system.addがルールの追加、system.state["value1"] = 0がパラメータのセット、system.evaluate()が評価メソッドの呼び出し、system.grade(forFact: NSString(string: "fact1"))が結果の取得になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.9),
    ])
system.state["value1"] = 0
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.9

条件式がfalseの場合、結果は0になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.9),
    ])
system.state["value1"] = 1 // → 今回は1をセット
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.0

GKRuleは複数渡す事ができます。
複数の条件を満たす場合、結果は全ての条件の和になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.1),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), assertingFact: NSString(string: "fact1"), grade: 0.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.3

しかし結果は1.0以上にはならないので注意が必要です。
和が1.0以上の場合は、1.0が返ってきます。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 1.1),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), assertingFact: NSString(string: "fact1"), grade: 2.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 1.0

GKRuleの初期化のassertingFactをretractingFactに変えると、条件を満たした時にgradeを減算するようになります。
これも和の時と同じように0.0以下にはなりません。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.5),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), retractingFact: NSString(string: "fact1"), grade: 0.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.3

GKRuleはNSPredicateではなくブロックで評価をする事もできます。

GKRule(blockPredicate: { system in return true }, action: { system in print("条件を満たした") })

GKRuleのサブクラスを作って、そこに評価式を書くこともできます。
複雑な条件の時はこれを使うのも良さそうです。

class MyRule: GKRule {
    override func evaluatePredicate(in system: GKRuleSystem) -> Bool {
        return true
    }
    
    override func performAction(in system: GKRuleSystem) {
        print("条件を満たした")
    }
}

【Swift】ベジェ曲線を自前で描いてみる

ベジェ曲線って良く聞くんですが、イマイチ理解できてなかったので自前で描いてみました。
具体的にはベジェ曲線の座標を自分で計算して描画をしてみました。

ベジェ曲線の座標の求め方

ベジェ曲線の座標は制御点を使って求められます。
今回は下のように制御点が3つの場合のベジェ曲線を求めます。

f:id:llcc:20170504212742p:plain

まずはP1とP2、P2とP3の2点間をつなぎます。

f:id:llcc:20170504213146p:plain

次にP1-P2間で少しだけP1から離れた点P4とP2-P3で少しだけP2から離れたP5を決めて、それらをつなぎます。

f:id:llcc:20170504213445p:plain

そしてP4-P5間で少しだけP4から離れた点P6がベジェ曲線の座標になります。

f:id:llcc:20170504213602p:plain

段々とP4・P5をP2・P3に近づけて行きつつ、それぞれのP6を算出します。

f:id:llcc:20170504213755p:plain

f:id:llcc:20170504213911p:plain

ここで算出したP6を全てつなげればベジェ曲線になります。
今回はこれをSwiftで実装してみようと思います。

ベジェ曲線をSwiftで描画

今回はこの3つの制御点を使ったベジェ曲線を求めます。

f:id:llcc:20170504214739p:plain

現状のコードは下の通りです。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let myView = MyView(frame: view.bounds)
        myView.backgroundColor = UIColor.white
        view.addSubview(myView)
    }
}

class MyView: UIView {
    let point1 = CGPoint(x: 100, y: 200)
    let point2 = CGPoint(x: 200, y: 100)
    let point3 = CGPoint(x: 300, y: 200)
    
    override func draw(_ rect: CGRect) {
        UIColor.darkGray.setFill()
        UIBezierPath(roundedRect: CGRect(origin: point1, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
        UIBezierPath(roundedRect: CGRect(origin: point2, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
        UIBezierPath(roundedRect: CGRect(origin: point3, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
    }
}

先程描いたように、point1・point2・point3を元に、point6を求めてそれを繋いでいきます。

class MyView: UIView {
    let point1 = CGPoint(x: 100, y: 200)
    let point2 = CGPoint(x: 200, y: 100)
    let point3 = CGPoint(x: 300, y: 200)
    
    override func draw(_ rect: CGRect) {
        let count = 100
        let path = UIBezierPath()
        path.move(to: point1)
        (0...count).forEach {
            let point4 = CGPoint(
                x: point1.x + (point2.x - point1.x) * CGFloat($0) / CGFloat(count),
                y: point1.y + (point2.y - point1.y) * CGFloat($0) / CGFloat(count)
            )
            let point5 = CGPoint(
                x: point2.x + (point3.x - point2.x) * CGFloat($0) / CGFloat(count),
                y: point2.y + (point3.y - point2.y) * CGFloat($0) / CGFloat(count)
            )
            let point6 = CGPoint(
                x: point4.x + (point5.x - point4.x) * CGFloat($0) / CGFloat(count),
                y: point4.y + (point5.y - point4.y) * CGFloat($0) / CGFloat(count)
            )
            path.addLine(to: point6)
        }
        path.addLine(to: point3)
        path.lineWidth = 5.0
        UIColor.brown.setStroke()
        path.stroke()
    }
}

これを実行すると以下のようになります。
無事にきれいな曲線を引くことができました。

f:id:llcc:20170504215426p:plain