とあるお兄さんの雑記

基本的に技術系の内容を書きますが、何を書くかは私の気分です。

見よう見まねでテスト駆動開発〜その3〜

テスト駆動開発(TDD)の第三弾です。

第一弾の記事
第二弾の記事

環境

macOS BigSur メモリ16G
Java 11.0.9.1
JUnit5 
IntelliJ IDEA CE 2020.3

今回作成するもの

今回作成するものは、Javaを一通り学んだ(とは言ってもメソッドを学んだぐらいの段階ですが)大学生の私が深夜テンションで作った、西暦と月と日にちを与えればその日の曜日を計算できる機能です。 (勘の良い方は元ネタが何かわかるかと思います。サマーウォーズの序盤の序盤で出てきた奴です。「サマーウォーズ モジュロ演算」とかでググれば出てきます。)

今回作るこの機能を、この記事ではモジュロ演算システムと呼ぶようにし、テスト駆動で開発してみることにします。

前回まで

前回は西暦の上2桁を取得するという非常にシンプルなテストだけを書いて終わりました。テストクラス(ModuloTest.java)と、実際のクラス(Modulo.java)は以下のようになっています。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }

    @Test
    public void testGetFirstAndLast2Digits(){
        Modulo modulo = new Modulo(2021, 1, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(20, modulo.getLast2Digits());
    }

    @Test
    public void testGetFirstAndLast2Digits2(){
        Modulo modulo = new Modulo(2000, 1, 1);
        assertEquals(19, modulo.getFirst2Digits());
        assertEquals(99, modulo.getLast2Digits());
    }


    @Test
    public void testGetMonth(){
        Modulo modulo = new Modulo(2021, 2, 1);
        assertEquals(14, modulo.getMonth());
    }
}

Modulo.java

package modulo;


public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.day = day;
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){
        month = calcMonth();
        return month;
    }

    public int getDay(){
        return day;
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }

    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
}

そして、現在のTODOリストは以下のようになっています。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

現在取り組んでいるものは太字、終わったものに関しては取り消し線で表すことにします。

チェラーの公式を使って曜日を出力する

チェラーの公式を使って計算しましょう。例によってテストから。

ModuloTest.java

〜略〜
    @Test
    public void testZeller(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals("木", modulo.calcZeller());
    }
〜略〜

テストはこけるので、これを通すように作っていきます。

Modulo.java

〜略〜
    public String calcZeller(){
        return "木";
    }
〜略〜

通りました。あとはこれをリファクタリングしていきます。

ここで、チェラーの公式は以下のようなものでした。ここでチェラーの公式で使われている文字と、実際のコードの変数との対応関係は以下になります。

 \displaystyle h = \left( q + \left\lfloor \frac{26(m+1)}{10} \right\rfloor  + K  + \left\lfloor \frac{K}{4} \right\rfloor + \left\lfloor \frac{J}{4} \right\rfloor  -2J \right) \bmod   7

文字 変数
 q day
 m month
 K last2Digits
 J first2Digits

そして、 hに対する数字と曜日の関係は以下になります。

 h 曜日
0 土曜日
1 日曜日
2 月曜日
3 火曜日
4 水曜日
5 木曜日
6 金曜日

以上の情報を元に作っていきます。

Modulo.java

package modulo;

import java.util.HashMap;
import java.util.Map;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;
    private int h; //追記

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.h = 0;//追記
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.day = day;
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){
        month = calcMonth();
        return month;
    }

    public int getDay(){
        return day;
    }

    public String calcZeller(){
        h = (day + 26*(month+1)/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7; // 追記
        return "木";
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }

    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
}

ここで、チェラーの公式で計算を行っているところで、- 2*first2Digitsと書くべきところを+ 5*first2Digitsと書いています。これは、多くのコンピュータの環境では負数の剰余を保証しません。(ってWikipediaに書いてありました。ちなみに、上記のチェラーの公式を使うのであれば、西暦1582年以降じゃないと無理っぽいですね。)

何を言っているかというと、余りを計算する場合、余りの値としては正の値が出てきて欲しいのですが、Javaに限らず他のプログラミング言語やコンピュータの環境では負の値を出してしまいます。例えば、

 -2 \div 7の余りは 5

という風に、余りの値を正として出して欲しいのですが、Javaだと

 -2 \div 7の余りは -2

と余りの値は負の値となってしまいます。チェラーの公式に当てはめるのであれば、正の値を出す必要があります。

そこで、整数の合同関係を使って計算することで、マイナスの値をプラスの値に変換することができます。

意外とこの合同関係って使うらしく、以下の動画でも使われています。6分あたりから合同式をうまく使って、テトリスの左回転を計算しています(なかなか面白いので、最初から見るのをお勧めします)。

www.youtube.com



最後に hの数値を元に曜日を出します。ここはif文でもswitch文でもいいのですが、せっかくなので、Mapを使ってみましょう。 hの数値をkeyとして、曜日を返すようなMapです。

Modulo.java

package modulo;

import java.util.HashMap;
import java.util.Map;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;
    private int h;
    private Map<Integer, String> map;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.h = 0;
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.day = day;

  //追記
        map = new HashMap<>(){
            {
                put(0, "土");
                put(1, "日");
                put(2, "月");
                put(3, "火");
                put(4, "水");
                put(5, "木");
                put(6, "金");
            }
        };
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){
        month = calcMonth();
        return month;
    }

    public int getDay(){
        return day;
    }

    public String calcZeller(){
        first2Digits = getFirst2Digits();//追記
        last2Digits = getLast2Digits(); //追記

        h = (day + (26*(month+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7;
        return map.get(h); //変更
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }

    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
}

mapという変数を宣言しました(変数名のセンスの無さよ...)。これで hをkeyとして曜日を取得することができます。
さて、これでテストを実行すると全て通ることが確認できるかと思います。

TODOリストを更新しましょう。ついでに、入力できる西暦も1582年〜というのも追加しておきましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する
  • 西暦の入力を1582年以降に制限する

西暦の入力を1582年以降に制限する

上記で紹介しているチェラーの公式を使うには、1582年以降(グレゴリオ暦)出ないとちゃんと計算できません。ユリウス歴や紀元前も計算に含めればいい練習になるとは思うのですが、面倒になってきたのでやりません。

さて、例によってテストから。

ModuloTest.java

〜略〜
    @Test
    public void testYearLimit(){
        Modulo modulo = new Modulo(1581, 4, 1);
        assertEquals("西暦は1582年以降を入力してください", modulo.calcZeller());
    }
〜略〜

テストは落ちるので、これを通すようにします。

Modulo.java

〜略〜
    public String calcZeller(){
        if(year < 1582){
            return "西暦は1582年以降を入力してください";
        }else{
            first2Digits = getFirst2Digits();
            last2Digits = getLast2Digits();

            h = (day + (26*(month+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7;
            return map.get(h);
        }
    }
〜略〜

これでテストは通るようになりました。TODOリストを更新します。また、上記テストでは2021年の4月をテストしていますが、念の為、1月、2月の時もちゃんと出るか、また、西暦が2000年で月が1月、2月の場合とそうでない月の場合で正しい曜日が出るかをテストとして追加して確認してみましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する
  • 西暦の入力を1582年以降に制限する
  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する

テスト項目の追加

2021年1月30日を入力として与えると、土曜日を出力する

TODOリストをみやすくしましょう。すでに終わったタスクは消します。

TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する

ModuloTest.java

〜略〜
    @Test
    public void testZeller2(){
        Modulo modulo = new Modulo(2021, 1, 30);
        assertEquals("土", modulo.calcZeller());
    }
〜略〜

上記テストを追加しテストを実行すると、テストは通りませんでした。まじか...。ここまできてテストが通らないのは辛い...。
さて、原因を探ってみると、Modulo.javacalcZellerメソッドの中身で使われている月がmonthのままでした。さらに探るとgetMonthメソッドがテストの時しか呼び出されておらず、さらにmonthという変数に代入してしまっています。

ModuloTest.java

〜略〜
    public int getMonth(){
        month = calcMonth();
        return month;
    }
〜略〜


ここで落ち着いて考えてみると、今のままだと、calcZellerメソッドからgetMonthメソッドを呼び、さらにgetMonthメソッドからcalcMonthメソッドを呼び出すことになってしまい、なんだか二度手間です。

それよりもcalcZellerメソッドからcalcMonthメソッドの処理を呼んで、newMonthという新しい変数名に代入し、newMonthをチェラーの公式の中で使うようにしたほうが良さそうです。

つまり、

Modulo.java

package modulo;

import java.util.HashMap;
import java.util.Map;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private int newMonth;//追記
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;
    private int h;
    private Map<Integer, String> map;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.h = 0;
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.newMonth = 0; //追記
        this.day = day;

        map = new HashMap<>(){
            {
                put(0, "土");
                put(1, "日");
                put(2, "月");
                put(3, "火");
                put(4, "水");
                put(5, "木");
                put(6, "金");
            }
        };
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){//中身の処理を変更
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }

    public int getDay(){
        return day;
    }

    public String calcZeller(){
        if(year < 1582){
            return "西暦は1582年以降を入力してください";
        }else{
            first2Digits = getFirst2Digits();
            last2Digits = getLast2Digits();
            newMonth = getMonth(); //追記

            h = (day + (26*(newMonth+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7;
            return map.get(h);
        }
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }

    //使わない
    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
}

こんな感じです。これでテストを走らせると全てグリーンになりました。あとはModulo.javacalcMonthメソッドは使わなくなったので、それを消してリファクタリング終了です。TODOリストも更新します。

TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する



2000年7月1日を入力として与えると、土曜日を出力する

次のTODOリストに残っているタスクを確認し、取り組みましょう。

TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する



ModuloTest.java

〜略〜
    @Test
    public void testZeller3(){
        Modulo modulo = new Modulo(2000, 7, 1);
        assertEquals("土", modulo.calcZeller());
    }
〜略〜

テストを走らせると、全て通ることを確認できます。他に特にやることはないので、TODOリストを更新して終了です。

TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する

2000年2月1日を入力として与えると、火曜日を出力する

TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する



ModuloTest.java

〜略〜
    @Test
    public void testZeller4(){
        Modulo modulo = new Modulo(2000, 2, 1);
        assertEquals("火", modulo.calcZeller());
    }
〜略〜

テストを走らせると、全て通ることを確認できます。他に特にやることはないので、TODOリストを更新して終了です。



TODOリスト


  • 2021年1月30日を入力として与えると、土曜日を出力する
  • 2000年7月1日を入力として与えると、土曜日を出力する
  • 2000年2月1日を入力として与えると、火曜日を出力する

これでTODOリストのタスク全てを消化できました。これで作りたかったモジュロ演算システムは完成です。お疲れ様でした。

TDDを一通りやってみて

最後はテストが通らないという事件もありましたが、無事最後までやり遂げることができました。途中、私が疲れてしまったところもあり雑に書いてしまった部分もあるかと思います。
とはいえ、ある程度TDDの一通りの手順は理解いただけたのでは無いでしょうか?

私自身、実際にTDDで開発してみて、変数名やメソッド名の付け方、クラスを分けるなど、どの部分がスキル的に足りないかというのを見つめ直すいい機会になりました。足りない部分に関してはまた一から勉強していければと考えています。

今回、TDDでの開発を3回に渡って書き記してきましたが、ここで書いた内容はTDD全体の一部でしかありません。本気でTDDを学びたいのであれば、書籍や他の記事を参考にしつつ、ご自身の手で書いていくことが一番身につきやすいと思います。

また、TDD全てを真似する必要はありませんし、人それぞれ向き不向きがあるかと思います。TDDや他の開発手法のいいとこ取りをしつつ、自分なりの開発手法を身につけていけばいいの ではないでしょうか?

今回の記事が皆さんのお役に立てれば幸いです。

ソースコード

Modulo.java

package modulo;

import java.util.HashMap;
import java.util.Map;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private int newMonth;
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;
    private int h;
    private Map<Integer, String> map;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.h = 0;
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.newMonth = 0;
        this.day = day;

        map = new HashMap<>(){
            {
                put(0, "土");
                put(1, "日");
                put(2, "月");
                put(3, "火");
                put(4, "水");
                put(5, "木");
                put(6, "金");
            }
        };
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }

    public int getDay(){
        return day;
    }

    public String calcZeller(){
        if(year < 1582){
            return "西暦は1582年以降を入力してください";
        }else{
            first2Digits = getFirst2Digits();
            last2Digits = getLast2Digits();
            newMonth = getMonth();

            h = (day + (26*(newMonth+1))/10 + last2Digits + last2Digits/4 + first2Digits/4 + 5*first2Digits) % 7;
            return map.get(h);
        }
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }
}

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }

    @Test
    public void testGetFirstAndLast2Digits(){
        Modulo modulo = new Modulo(2021, 1, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(20, modulo.getLast2Digits());
    }

    @Test
    public void testGetFirstAndLast2Digits2(){
        Modulo modulo = new Modulo(2000, 1, 1);
        assertEquals(19, modulo.getFirst2Digits());
        assertEquals(99, modulo.getLast2Digits());
    }


    @Test
    public void testGetMonth(){
        Modulo modulo = new Modulo(2021, 2, 1);
        assertEquals(14, modulo.getMonth());
    }

    @Test
    public void testZeller(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals("木", modulo.calcZeller());
    }

    @Test
    public void testYearLimit(){
        Modulo modulo = new Modulo(1581, 4, 1);
        assertEquals("西暦は1582年以降を入力してください", modulo.calcZeller());
    }

    @Test
    public void testZeller2(){
        Modulo modulo = new Modulo(2021, 1, 30);
        assertEquals("土", modulo.calcZeller());
    }

    @Test
    public void testZeller3(){
        Modulo modulo = new Modulo(2000, 7, 1);
        assertEquals("土", modulo.calcZeller());
    }

    @Test
    public void testZeller4(){
        Modulo modulo = new Modulo(2000, 2, 1);
        assertEquals("火", modulo.calcZeller());
    }
}

見よう見まねでテスト駆動開発〜その2〜

テスト駆動開発(TDD)の第二弾です。
TDDとは何か、なぜやろうと思ったのかなどについては、前回の記事をご覧頂ければと思います。

第一弾の記事

環境

macOS BigSur メモリ16G
Java 11.0.9.1
JUnit5 
IntelliJ IDEA CE 2020.3

今回作成するもの

今回作成するものは、Javaを一通り学んだ(とは言ってもメソッドを学んだぐらいの段階ですが)大学生の私が深夜テンションで作った、西暦と月と日にちを与えればその日の曜日を計算できる機能です。
(勘の良い方は元ネタが何かわかるかと思います。サマーウォーズの序盤の序盤で出てきた奴です。「サマーウォーズ モジュロ演算」とかでググれば出てきます。)

今回作るこの機能を、この記事ではモジュロ演算システムと呼ぶようにし、テスト駆動で開発してみることにします。

前回まで

前回は西暦の上2桁を取得するという非常にシンプルなテストだけを書いて終わりました。テストクラス(ModuloTest.java)と、実際のクラス(Modulo.java)は以下のようになっています。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testFirst2Digits(){
        Modulo modulo = new Modulo();
        assertEquals(20, modulo.getFirst2Digits(2021));
    }
}

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;

    public Modulo(){
        first2Digits = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }
}

そして、現在のTODOリストは以下のようになっています。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

現在取り組んでいるものは太字、終わったものに関しては取り消し線で表すことにします。

今回はTODOリストから「2021年の下二桁を取得する」を取り出してこの機能を作っていくことにします。
(西暦下二桁の取得だけじゃなく、月や、日にちの取得もまとめてやっちゃってもいいんじゃないの?と思われるかもしれません。ただ今回は、小さいステップ(スモールステップ)でやろうと思っていますので、多少煩雑に思われるかもしれませんが、お付き合い願います。)

西暦の下二桁を取得する

まずはテストから書いていきましょう!

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo();
        assertEquals(20, modulo.getFirst2Digits(2021));
        assertEquals(21, modulo.getLast2Digits(2021)); //追加
    }
}

メソッド名はtestFirst2DigitsからtestGetMethodに変更しました。というのも、下2桁のテストを加えたことで、上2桁だけのテストではなくなったためです。

テストを走らせてみます。すると当たり前ですがテストはこけます。Modulo.javaにgetLast2Digitsメソッドを作っていないためです。このテストを通すようにするため、Modulo.javaを以下のように書いてみます。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;

    public Modulo(){
        first2Digits = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }

    public int getLast2Digits(int year){
        return 21;
    }
}

これでテストは通るようになります。ですが、このままだとどの西暦を入れても、 21という数字を返すことになるため、西暦の下二桁を返すようにリファクタリングします。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;
    private int last2Digits;

    public Modulo(){
        first2Digits = 0;
        last2Digits = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }

    public int getLast2Digits(int year){
        last2Digits = year % 100;
        return last2Digits;
    }
}

これでテストは通るようになりました!TODOリストを更新します。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

あとは月の取得、日にちの取得を順にやっていけばいいのですが、ここでは西暦の上2桁、下2桁の取得とやり方はほぼ同じなので省略します。

少し立ち止まる

さて、西暦の下二桁、上二桁、月、日にちを取得して返す機能のテストクラスと実際のクラスは以下のようになりました。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int month;
    private int day;

    public Modulo(){
        first2Digits = 0;
        last2Digits = 0;
        month = 0;
        day = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }

    public int getLast2Digits(int year){
        last2Digits = year % 100;
        return last2Digits;
    }

    public int getMonth(int month){
        this.month = month;
        return month;
    }

    public int getDay(int day){
        this.day = day;
        return day;
    }
}

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo();
        assertEquals(20, modulo.getFirst2Digits(2021));
        assertEquals(21, modulo.getLast2Digits(2021));
        assertEquals(4, modulo.getMonth(4));
        assertEquals(1, modulo.getDay(1));
    }
}

で、私としては、なんか違和感を覚えてしまって...。
Modulo.javaの各getメソッドは値を取得するだけのものなので、引数を受け取るのはおかしいんじゃない?と思いました。加えて、オブジェクトを作った時に引数として、西暦、月、日にちを渡した方がいいのでは?と考えました。
つまり、テストで書くとこうなります。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }
}

このテストを通すために、以下のようにModulo.javaのクラスを以下のように書きます。 Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int month;
    private int day;

    public Modulo(int year, int month, int day){
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public int getFirst2Digits(){
        first2Digits = year / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        last2Digits = year % 100;
        return last2Digits;
    }

    public int getMonth(){
        this.month = month;
        return month;
    }

    public int getDay(){
        this.day = day;
        return day;
    }
}

Modulo.javaのgetメソッドは大分シンプルになったように感じます。this.month = month;などの重複部分があるので、そこだけ消しましょう。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int month;
    private int day;

    public Modulo(int year, int month, int day){
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public int getFirst2Digits(){
        first2Digits = year / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        last2Digits = year % 100;
        return last2Digits;
    }

    public int getMonth(){
        return month;
    }

    public int getDay(){
        return day;
    }
}

今回少し立ち止まったのは、TDDなどとは関係ないかもしれませんが、もっとよく書けるんじゃないか?という考えは大事に思えます(元々の原因は西暦と月と日にちの入力をどうするかを全く考えていなかったためですが)。

月が1月または2月だった場合は、それぞれ前年の13月、14月に変換する機能の実装

現在のTODOリストは以下のようになっています。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

TODOリストから「月が1月または2月だった場合は、それぞれ前年の13月、14月に変換」をとってきて、これを作業中にします。が、この作業、よくみると以下の2つに分けられそうです。

  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す

これら2つをTODOリストに書いて、まず「月が1もしくは2の時に西暦を-1する」機能を実装することにしましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

月が1もしくは2の時に西暦を-1する

まずは例によってテストから書いていきます。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }

    @Test
    public void testGetFirstAndLast2Digits(){
        Modulo modulo = new Modulo(2021, 1, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(20, modulo.getLast2Digits());
    }
}

テストがこけたので、このテストを通すようにします。

Modulo.java

    public int getLast2Digits(){
        if(month == 1 || month == 2){
            year = year - 1;
        }
        last2Digits = year % 100;
        return last2Digits;
    }

これでテストは通るようになりました。が、getLast2Digits()の中に、月が1月または2月なのかそれ以外の月なのかという処理をしています。できれば、getLast2Digits()の中の処理は西暦の下2桁だけを返すようにしたいです。

というわけで、設計として正しいかどうかは置いておき、新しい機能として月が1月または2月だったら、戻り値にyear - 1を、それ以外の月であれば戻り値にyearを設定するメソッドを作成します。

Modulo.java

    public int getLast2Digits(){
        year = calcYearFromMonth(month);
        last2Digits = year % 100;
        return last2Digits;
    }
〜省略〜

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return year - 1;
        }else{
            return year;
        }
    }

このように書けばテストが通り、月が1月または2月の時の西暦の計算ができるようになりました。TODOリストを更新します。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

コラム:設計としての正しさ?

先ほど月が1月もしくは2月だった場合の西暦を計算する機能を作成しましたが、設計として正しいかと言われると正しくはないでしょう。

例えば、増田先生の著書である現場で役立つシステム設計の原則では、intなどの基本データ型を使うのはあまりよろしくないとされています(29ページから)。

というのも、intはマイナス21億からプラス21億までの値を扱うことができます。しかし、実際の業務ではこんなに大きな値を使う可能性は低いですし、思わぬ障害が混入する原因になります。ましてや、月であれば 1 12まで、日にちであれば 1 31までしか必要ありません。むやみやたらにintなどを使うのはやめたほうがいいでしょう。

そうなると、なぜ今回はやらないのかというと面倒だからです。それと今回のメインはあくまでテスト駆動開発だということで、その中にシステム設計の話なども盛り込むとややこしくなる可能性が高いのと、テスト駆動開発とシステム設計の話をうまくまとめられる自信がないため今回の記事ではスキップさせていただきます。

ちなみに、増田先生の著書はこちらです。エンジニア1〜3年目の方はほぼ必読、ベテランエンジニアの方でも役立つ内容が多いため非常におすすめの本です。

現場で役立つシステム設計の原則

コラム:privateメソッドはテストしなくていいのか?

先ほど月が1月もしくは2月だった場合の西暦を計算する機能を作成しましたが、そのメソッドはpublicではなく、privateでした。privateで宣言するとそのクラス内でしか参照することができません。つまり、publicで書かない限りテストを書くことはできません。

じゃあ全てpublicで書かないといけないかというと、そうではないように思います。実際、月の計算は外部から参照することなく内部のみで呼び出しているので、わざわざpublicにする必要はありません。
じゃあどうすれば良いのか。

こちらに関してはテスト駆動開発の翻訳者である和田さんがブログで回答されています。

t-wada.hatenablog.jp

詳しい内容は上記ブログを参考にして貰えれば良いのですが、結論だけ書けば、ほとんどのプライベートメソッドはパブリックメソッド経由でテストを行うことができるため、プライベートメソッドのテストを書く必要は無いです。

テストの追加

先ほど、月が1もしくは2の時に西暦を-1するという機能を作りました。作ったはいいのですが、西暦が仮に2000年の時、ちゃんと1999年が返ってくるでしょうか?一旦このことをTODOリストに書いて、それに取り組むことにします。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に2000年を-1し、1999年が取得できるかを確認
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する



テストを書いて確認してみましょう。

ModuloTest.java

〜略〜
    @Test
    public void testGetFirstAndLast2Digits2(){
        Modulo modulo = new Modulo(2000, 1, 1);
        assertEquals(19, modulo.getFirst2Digits());
        assertEquals(99, modulo.getLast2Digits());
    }
〜略〜

見事にテストがこけました。どうやら西暦の上2桁が合っていないようです。calcYearFromMonthメソッドを西暦の下二桁でしか呼んでいないからでしょう。加えて、calcYearFromMonthメソッドの中の処理も怪しいです。yearという変数に代入してしまっているため、常に値が変わってしまいます。
そこで、一旦ここはlastYearという変数を作りましょう。lastYearという変数を使う方がバグの発生などを減らせます。

Modulo.java

〜略〜
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear; //追記
    private int month;
    private int day;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.lastYear = this.year - 1; //追記
        this.month = month;
        this.day = day;
    }
〜略〜

このように書きました。加えて、月が1もしくは2の時に返す西暦を次のように書き換えます。

Modulo.java

〜略〜
    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }
〜略〜

また、getFirst2DigitsgetLast2Digitsメソッドないで受け取る変数もyearとして書き換えるのではなく、新しくnewYearという変数名にしましょう(newYearという変数は新年を思い浮かべるため、あまりよろしくないのですが、他にいい案が思いつかなかったため一旦newYearとして突き進みます)。

Modulo.java

〜略〜
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear; //追記
    private int month;
    private int day;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.lastYear = this.year - 1; //追記
        this.month = month;
        this.day = day;
    }
〜略〜
    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }
〜略〜

これで、テストが通るようになりました。TODOリストを更新します。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に2000年を-1し、1999年が取得できるかを確認
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

月が1もしくは2の時に12を足す(13月、14月にする)

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に2000年を-1し、1999年が取得できるかを確認
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

「月が1もしくは2の時に西暦を-1する」という機能は作ったので、次に「月が1もしくは2の時に12を足す」という機能を作ります。例によってテストから。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }

    @Test
    public void testGetFirstAndLast2Digits(){
        Modulo modulo = new Modulo(2021, 1, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(20, modulo.getLast2Digits());
    }

    @Test
    public void testGetFirstAndLast2Digits2(){
        Modulo modulo = new Modulo(2000, 1, 1);
        assertEquals(19, modulo.getFirst2Digits());
        assertEquals(99, modulo.getLast2Digits());
    }


    @Test
    public void testGetMonth(){
        Modulo modulo = new Modulo(2021, 2, 1);
        assertEquals(14, modulo.getMonth());
    }
}

当たり前ですがテストは通りません。というわけで、これを通すようにします。

Modulo.java

〜略〜
    private int calcMonth(){
        if(month == 1 || month == 2){
            return month + 12;
        }else{
            return month;
        }
    }
〜略〜

これで一応通りますが、これもメソッド内でmonthの値を書き換えてしまっています。これをやると先ほども書いたように思わぬバグを招いていしまいますので、新しい変数を用意してそれを返すようにしましょう。
Enumを使うという方法もありますが、ここはより簡単にJANUARYFEBRUARYという定数を用意して、予め13と14という値を入れてmonthが1月、2月の時はその定数を返す方法にしてみましょう。

Modulo.java

〜略〜
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
〜略〜
    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
〜略〜

テストが通ったことでリファクタリングも無事に終わりました。TODOリストを更新しましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 月が1もしくは2の時に西暦を-1する
  • 月が1もしくは2の時に2000年を-1し、1999年が取得できるかを確認
  • 月が1もしくは2の時に12を足す
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

残るはチェラーの公式を使って曜日を計算するところですね。長くなりましたので、今回はここまでとします。

ここまでのソースコード

Modulo.java

package modulo;


public class Modulo {
    private int first2Digits;
    private int last2Digits;
    private int year;
    private int lastYear;
    private int newYear;
    private int month;
    private final int JANUARY = 13;
    private final int FEBRUARY = 14;
    private int day;

    public Modulo(int year, int month, int day) {
        this.first2Digits = 0;
        this.last2Digits = 0;
        this.year = year;
        this.lastYear = this.year - 1;
        this.newYear = 0;
        this.month = month;
        this.day = day;
    }

    public int getFirst2Digits(){
        newYear = calcYearFromMonth();
        first2Digits = newYear / 100;
        return first2Digits;
    }

    public int getLast2Digits(){
        newYear = calcYearFromMonth();
        last2Digits = newYear % 100;
        return last2Digits;
    }

    public int getMonth(){
        month = calcMonth();
        return month;
    }

    public int getDay(){
        return day;
    }

    private int calcYearFromMonth(){
        if(month == 1 || month == 2){
            return lastYear;
        }else{
            return year;
        }
    }

    private int calcMonth(){
        if(month == 1){
            return JANUARY;
        }else if(month == 2){
            return FEBRUARY;
        }else{
            return month;
        }
    }
}

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testGetMethod(){
        Modulo modulo = new Modulo(2021, 4, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(21, modulo.getLast2Digits());
        assertEquals(4, modulo.getMonth());
        assertEquals(1, modulo.getDay());
    }

    @Test
    public void testGetFirstAndLast2Digits(){
        Modulo modulo = new Modulo(2021, 1, 1);
        assertEquals(20, modulo.getFirst2Digits());
        assertEquals(20, modulo.getLast2Digits());
    }

    @Test
    public void testGetFirstAndLast2Digits2(){
        Modulo modulo = new Modulo(2000, 1, 1);
        assertEquals(19, modulo.getFirst2Digits());
        assertEquals(99, modulo.getLast2Digits());
    }


    @Test
    public void testGetMonth(){
        Modulo modulo = new Modulo(2021, 2, 1);
        assertEquals(14, modulo.getMonth());
    }
}

参考

テスト駆動開発

現場で役立つシステム設計の原則

t-wada.hatenablog.jp

見よう見まねでテスト駆動開発〜その1〜

テスト駆動開発ですよ、お兄さん(特に意味はない)。

テスト駆動開発とは?

世の中には色々な開発手法があるのですが、開発手法の一つにテスト駆動開発というものがあります。テスト駆動開発(Test-Driven Development: TDD)というのは文字通りテストを最初に(テストファーストで書いていくものです。

普通であれば、作りたい機能を作ってから、その機能がちゃんと動いているのかという自動化テストを書いていきます。
一方、テスト駆動開発作りたい機能のテストを先に書いて、開発を進めていく開発スタイルです。具体的な手順は後々見ていくとして、テスト駆動開発のルールとしては2つです。

  1. 自動化されたテストが失敗した時のみ、新しいコードを書く
  2. 重複を除去する

以上2つのみです。シンプルですね。

ちなみに、テスト駆動開発有名な方だと和田卓人さんという方がいらっしゃいます。
また、以下の本も有名です。私はTDDが気になっていたということもあり、買いました。

テスト駆動開発

TDDに取り組もうとしたきっかけ

会社で開発時、私が全くと言っていいほどテストを書かなかったためです。まぁ、学生の頃ユニットテストの「ユ」の字も知らなかったので仕方ないといえば仕方ないのですが。

ひとまず、先輩に言われてから少しずつテストを書くようになったのはいいのですが、せっかくだしテストについて勉強してみようと思い、前々から気になっていたTDDを学んでみようと思ったわけです。

実際にやってみて

で、上記の本(テスト駆動開発)を買った上で、TDDをやってみたはいいんですが、いつの間にか本に書いてあることを理解するより、本を読み進めることを目標にしていました...。そのため、第1部を読み切ったはいいのですが、全く理解していないという状況に陥ってしまいました。テストを学びたいという気持ちはどこへやら....。

流石にこれだと学んだことが実際に活かしにくいので、それならもうちょっと簡単な例でテスト駆動開発(もどき)をやってみよう!と思い立ったので実際にやってみることにします。


※今回の記事では、一応TDDのスタイルである機能を作っていくことにしますが、実際の現場ではTDDのルール全てに従う必要はないかなと思います。そこら辺は臨機応変にいきましょう。

環境

macOS BigSur メモリ16G
Java 11.0.9.1
JUnit5 
IntelliJ IDEA CE 2020.3

今回作成するもの

今回作成するものは、Javaを一通り学んだ(とは言ってもメソッドを学んだぐらいの段階ですが)大学生の私が深夜テンションで作った、西暦と月と日にちを与えればその日の曜日を計算できる機能です。
(勘の良い方は元ネタが何かわかるかと思います。サマーウォーズの序盤の序盤で出てきた奴です。「サマーウォーズ モジュロ演算」とかでググれば出てきます。)

今回作るこの機能を、この記事ではモジュロ演算システムと呼ぶようにし、テスト駆動で開発してみることにします。

テスト駆動開発の順序

  1. レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く
  2. グリーン:そのテストを迅速に動作させる。このステップでは罪を犯しても良い
  3. リファクタリング:テストを通すために発生した重複を全て除去する

ここでリファクタリングというのは、外部から見た振る舞いを変えずに、コードを書き換えて改善することです。この順序を元にモジュロ演算システムを作ってみましょう。

モジュロ演算システムを作る

TODOリスト

それでは、実際にモジュロ演算システムをTDDで作っていきましょう。
と、その前に、一旦ゴールを明確にしましょう。

テスト駆動開発ではTODOリストを作成して、何に取り組めば良いかを視覚化しているので、まずはこの本に乗っ取ってTODOリストから作成してみることにします。

TODOリスト


  • 西暦、月、日にちを入力として与えると、その曜日を出力する


まぁ、上記でもいいのですが、もうちょっと具体的に書いてみましょう。
例えば、2021年4月1日の曜日は木曜日ですので、以下のようにTODOリストを書き換えてみます。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する


これが、TDDとして正しいかどうかはプロの方に実際に見てもらうとして、目標としては具体的になりました。とは言っても、まだこれだと内部でどうやって計算すれば良いのか分からないので、複雑に感じます。
ここで、実際に求め方の過程を見てみることにします。参考にする記事はこちらです。

ameblo.jp

上記記事を参考にすると、

  1. 西暦、月、日にちを受け取る(例. 2021年4月1日)
  2. 西暦の千の位と百の位(2021なので、上2桁の20)を J、西暦の下2桁(2021なので、21)を K、月を m、日にちを q、曜日を hとする。
  3. ただし、求めたい日の月が1月の場合は前年の13月、2月の場合は14月とする。(仮に2021年の1月1日なら、2020年の1月1日として計算)
  4. チェラーの公式(ツェラーの公式)(後述)を使って、 hを計算する。
  5. 計算した hの値と曜日の対応は以下の通りである。
 h 曜日
0 土曜日
1 日曜日
2 月曜日
3 火曜日
4 水曜日
5 木曜日
6 金曜日

ここで、チェラーの公式(ツェラーの公式)を簡単に紹介します。

 \displaystyle h = \left( q + \left\lfloor \frac{26(m+1)}{10} \right\rfloor  + K  + \left\lfloor \frac{K}{4} \right\rfloor + \left\lfloor \frac{J}{4} \right\rfloor  -2J \right) \bmod   7

ここで、 \displaystyle  \left\lfloor N \right\rfloor   Nを超えない最大の整数(床関数と呼ばれる)、 \displaystyle x \bmod n x nで割った剰余(余り)を表します。

また、床関数についてですが、例えば \displaystyle  \left\lfloor 5.8 \right\rfloor  だった場合、 \displaystyle  \left\lfloor N \right\rfloor   Nを超えない最大の整数であるため、 \displaystyle  \left\lfloor 5.8 \right\rfloor  の計算結果は整数になります。
さらに、 Nを超えない最大の整数であることから、 5.8を超えない最大の整数を求めることになります。つまり、 5.8以下の整数( 5, 4, 3, \dots )で最大の数字は 5となるため、今回でいえば 5となります。

これで求める過程がわかったので、TODOリストに書き込んでいきましょう。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

おおよそ、こんなものでいいでしょう。一旦ここはこのままで進めていくとし、もし、必要なものが出てくればその都度TODOリストに追加していくとしましょう。

コラム:チェラーの公式を使って求めてみる。

簡単に紹介したところで、チェラーの公式に実際に値を代入して計算してみます。2021年4月1日を例に計算します。

 J = 20 K = 21 m = 4 q = 1より、

 \displaystyle \left( q + \left\lfloor \frac{26(m+1)}{10} \right\rfloor  + K  + \left\lfloor \frac{K}{4} \right\rfloor + \left\lfloor \frac{J}{4} \right\rfloor  -2J \right)

 \displaystyle  = \left( 1 + \left\lfloor \frac{26(4+1)}{10} \right\rfloor  + 21  + \left\lfloor \frac{21}{4} \right\rfloor + \left\lfloor \frac{20}{4} \right\rfloor  -2 \times 20 \right)

 \displaystyle  = \left( 1 + \left\lfloor 13 \right\rfloor  + 21 + \left\lfloor 5.25 \right\rfloor + \left\lfloor 5 \right\rfloor  -40 \right)

 \displaystyle  = 1 + 13  + 21  +5 + 5 - 40

 \displaystyle  =  45 - 40

 \displaystyle  =  5

よって、

 \displaystyle  h =  5 \bmod 7

 \displaystyle  h =  5

 \displaystyle  h =  5より、2021年4月1日は木曜日となることがわかり、実際のカレンダーをみても一緒だということがわかりました。

TODOリストを参考にテストを書く

現在のTODOリストは以下のようになっています。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

ここで、TODOリストから一つ取り出して作業を行っているものは太字で表すことにします。まずは「2021年の上二桁を取得」をとってくることにします。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

レッド

では、実際にテストを書いていきましょう。
TDDはなんと言っても、テストファーストです。どんなオブジェクトが必要かなんて考えることなく、以下のようにテストから先に書きます。

ModuloTest.java

package modulo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ModuloTest {
    @Test
    public void testFirst2Digits(){
        Modulo modulo = new Modulo();
        assertEquals(20, modulo.getFirst2Digits(2021));
    }
}

TDDを初めて学ぶ人からするとかなり面食らうと思います。というのも、Moduloというクラスも何もないのですから(私もびっくりしました)。ですが、これがTDDです。
TDDの順番として、

  1. レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く
  2. グリーン:そのテストを迅速に動作させる。このステップでは罪を犯しても良い
  3. リファクタリング:テストを通すために発生した重複を全て除去する

レッドの部分をやったことになります。

グリーン

というわけで次にグリーンを行います。
まず必要なのはModuloというクラスを作ることです。

Modulo.java

package modulo;

public class Modulo {
}


次に必要なのはgetFirst2Digitsメソッドです。しかも引数をもらい、戻り値のあるものなので、以下のように書きます(念の為、空のコンストラクタも書いておきましょう)。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){

    }
    
    public int getFirst2Digits(int year){
        return 0;
    }
}

これでエラーが無くなったので、テストを走らせてみます。

f:id:kurasher:20210330001028p:plain

エラーが出てきました。まぁ、当たり前ですが。さて、このテストをできるだけ早く通さないといけません。そのためには、罪を犯しても良いので、以下のように書きます(私は、罪を犯すの意味が今のところまだ理解できておりません)。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){
    }

    public int getFirst2Digits(int year){
        return 20;
    }
}

f:id:kurasher:20210330001554p:plain

今度はテストが通りましたので、ひとまず手順2のグリーンは突破しました。

リファクタリング

グリーンを突破したので、今度はリファクタリングが必要です。

本来Modulo.javaクラスの中にあるgetFirst2Digitsメソッドは西暦の上2桁を取得するものでした。というわけで、getFirst2Digitsのメソッドから上2桁を取得する処理を追加し、戻り値の20を削除することにします。

Modulo.java

package modulo;

public class Modulo {

    public Modulo(){
    }

    public int getFirst2Digits(int year){
        year = year / 100;
        return year;
    }
}

上記のように書いてみました。一応、上記でもテストは通るのですが、getFirst2Digitsメソッドは西暦の上2桁を first2Digitsという変数で返したほうがわかりやすいように思えます(個人の感覚になりますので、無理に合わせる必要はないですし、そういう考えもあるかぐらいに流していただければと思います)。

というわけで、 first2Digitsという変数をModulo.javaに追記します。ついでに、コンストラクタで初期化しておきます。

Modulo.java

package modulo;

public class Modulo {
    private int first2Digits;

    public Modulo(){
        first2Digits = 0;
    }

    public int getFirst2Digits(int year){
        first2Digits = year / 100;
        return first2Digits;
    }
}

これで、だいぶ見栄えがよくなりましたね!
以上で、リファクタリングの手順は終了です。TODOリストも更新しておきましょう。終わった作業に関しては取り消し線で表すことにします。

TODOリスト


  • 2021年4月1日を入力として与えると、木曜日を出力する
  • 2021年の上二桁を取得
  • 2021年の下二桁を取得
  • 月を取得
  • 月が1月または2月だった場合は、それぞれ前年の13月、14月に変換
  • 日にちを取得
  • チェラーの公式を使って曜日を出力する

次回

今回は長くなりましたので、この辺で終了です。
思ったよりもTDDの概要や、余計な話で持っていかれてしまったので、次回以降、より本格的にTDDでモジュロ演算システムを作成していきます。

統計学基礎vol.34〜母平均の信頼区間を求める(母分散が未知の場合)〜

前回の記事は、母分散が既知の場合の母平均の信頼区間を求めました。今回の記事は母分散が未知の場合の信頼区間を求めてみます。

しかし、実際の現場とかで、母平均を求めることってあるんでしょうか?母平均の信頼区間を求めるなんて、学生の統計学の試験か、統計検定ぐらいでしょうか?

母分散未知の場合の求め方は?

母分散が既知の場合、標準化を行うことで、標準正規分布を使って求めることができました。
しかし、母分散が未知の場合、標準化を行う際の分散が分からないため、標準化ができません。そこで母分散が分からない代わりに、不偏分散 s ^2を用いて区間推定を行います。また、不偏分散を使って区間推定を行う場合、 t分布を使います。

平均 \mu、不偏分散 s ^2正規分布に従う母集団から抽出したサンプルサイズ nの標本を使って算出される統計量 tの値は自由度 n-1 t分布に従います。よって、母分散が不明な場合の区間推定は、 t分布を使って行います。

コラム:t分布の性質

まぁ、ここは本筋(母分散が未知の場合の信頼区間を求める)とは外れるので、あまり深追いしなくていいですし、どちらかというと、統計Web 20-1. 標本とt分布の記事の丸パクリなので、無視でいいっちゃいいんですよね...。
自分のために書いていきますが...。
(正直、この辺りは専門家の方や、大学で真面目に勉強している理数系の学生の方が詳しい気がしますね...。)

t分布の成り立ち

標準正規分布 N(0,1)に従う Zと自由度 nカイ二乗分布 Wがあり、これらが互いに独立であるとき、次の式から算出される tは自由度 n t分布に従います。

 \displaystyle t = \frac{Z}{\sqrt{\frac{W}{n}}}

ここで、正規分布 N(\mu, \sigma ^2)に従う母集団から抽出したサンプルサイズ nの標本 (x_1, x_2, \dots, x_n)を考える。
定義から、 \displaystyle \frac{\bar{x} - \mu}{\sqrt{\frac{s ^2}{n}}}は標準正規分布 N(0,1)に、 \displaystyle \sum_{i=1} ^{n} \left(\frac{x_i - \bar{x}}{\sigma} \right) ^2は自由度 (n-1)カイ二乗分布に従う。これらを自由度 (n-1)とした時、上式に当てはめると、

 \displaystyle t = \frac{Z}{\sqrt{\frac{W}{n-1}}}

 \displaystyle = \frac{\frac{\bar{x} - \mu}{\frac{\sigma}{\sqrt{n}}}}{\sqrt{\frac{\sum_{i=1} ^{n} \left(\frac{x_i - \bar{x}}{\sigma} \right) ^2}{n-1}}}

 \displaystyle = \frac{\sqrt{n}(\bar{x} - \mu)}{\sqrt{\frac{(x_i - \bar{x}) ^2}{n-1}}}

 \displaystyle = \frac{\sqrt{n}(\bar{x} - \mu)}{\sqrt{s ^2}}

 \displaystyle = \frac{\bar{x} - \mu}{\sqrt{\frac{s ^2}{n}}}

ここで、 s ^2は不偏分散で、 tは自由度 (n-1) t分布に従う。
分からなくていいです。私も分からないので。
(じゃあ、記事に書くなって話ではあるんですが。)

t分布の期待値と分散

確率変数 Xが自由度 m t分布にしたがっているとき、 Xの期待値と分散は次のようになる。

 \displaystyle E[X] = 0 (m > 1)

 \displaystyle V[X] = \frac{m}{m-2} (m > 2)

母平均の信頼区間の求め方〜母分散未知〜

手順として大きく4つになります。

  1. 標本平均 \bar{x}と不偏分散 s ^2を求める。
  2. 統計量 tを計算する。
  3. 2で算出された統計量が t分布の100 \alpha \%の面積の範囲にあれば良い。
  4. 100 \alpha \%信頼区間を求める。

順に見ていきます。

1. 標本平均と不偏分散を求める

 \displaystyle \bar{x} = \frac{1}{n} \sum_{i=1} ^{n} x_i

 \displaystyle s ^2 = \frac{1}{n-1} \sum_{i=1} ^{n} (x_i - \bar{x}) ^2

2. 統計量を計算する

 \displaystyle t = \frac{\bar{x} - \mu}{\sqrt{\frac{s ^2}{n}}}

 \muは母平均ですね。母平均の信頼区間を求めたいので、正確には上の式は求められません。

3. 2で算出された統計量を計算する

ここで、自由度は (n-1)であることに注意する必要があります。

 t分布表でググると以下の記事がヒットしたので、こちらを参考にします。いや、統計Webさまさま。

bellcurve.jp

ここで、 t_\frac{\alpha}{2}(n-1) t (\alpha)で置き換えるとして、統計量 tは以下の範囲にあれば良いことになります。

 \displaystyle -t (\alpha) \leq t \leq t (\alpha)

ここで、 t_\frac{\alpha}{2}(n-1) \displaystyle \frac{\alpha}{2}となっているのは、前回と同じで、両側検定をするためです。
仮に 95\%信頼区間として求めるなら、上の記事から \alpha 0.025の列を見れば良いです。(2倍すれば 0.05、つまり 5\%)

さて、 \displaystyle  -t (\alpha) \leq t \leq t (\alpha)に2で求めた統計量 tを代入します。

 \displaystyle  -t (\alpha) \leq \frac{\bar{x} - \mu}{\sqrt{\frac{s ^2}{n}}} \leq t (\alpha)

4. 信頼区間を求める

 100 \alpha \%の信頼区間を求めます。手順3で見たように 95\%信頼区間であれば、 t分布表の \alpha 0.025の列を見れば良いです。
また、サンプルサイズは nですが、自由度は (n-1)となります。 t分布表でみる場合は、自由度を参考にするので (n-1)の行をみるように注意しましょう。

さて、本題の \muの信頼区間を求めてみましょう。 \muの幅を求めりゃいいので、

 \displaystyle  -t (\alpha) \leq \frac{\bar{x} - \mu}{\sqrt{\frac{s ^2}{n}}} \leq t (\alpha)

 \displaystyle  -t (\alpha) \sqrt{\frac{s ^2}{n}} \leq \bar{x} - \mu \leq t (\alpha) \sqrt{\frac{s ^2}{n}}

 \displaystyle  \bar{x} -t (\alpha) \sqrt{\frac{s ^2}{n}} \leq  \mu \leq \bar{x}  + t (\alpha) \sqrt{\frac{s ^2}{n}}

あとは、計算するだけで求まりますね。お疲れ様でした。

区間推定の特徴(母分散未知の場合)

信頼区間の幅は、
信頼係数 \alpha(0 \leq \alpha \leq 1)が小さいときほど
また
サンプルサイズ nが大きいほど
狭くなります。

この特徴ですが、母分散既知の場合と変わりません。

まとめ

今回の記事のまとめです。

~母分散未知の母平均の信頼区間の手順~

  1. 標本平均 \bar{x}と不偏分散 s ^2を求める。
  2. 統計量 tを計算する。
  3. 2で算出された統計量が t分布の100 \alpha \%の面積の範囲にあれば良い。
  4. 100 \alpha \%信頼区間を求める。

 \displaystyle  \bar{x} -t (\alpha) \sqrt{\frac{s ^2}{n}} \leq  \mu \leq \bar{x}  + t (\alpha) \sqrt{\frac{s ^2}{n}}


~信頼区間の特徴~
信頼係数 \alpha(0 \leq \alpha \leq 1)が小さいほど、また、サンプルサイズ nが大きいほど狭くなる

コラム:標準正規分布表の面積に注意?

前回と今回続けて、信頼区間の話でした。
信頼区間を求める際に非常に重要な役割をになっている標準正規分布表ですが、参考にする記事や書籍によっては求めている範囲が違うなんてことがあります。

たとえば、以下の記事では、 0 Z_0までの面積(確率)を求めている標準正規分布表を参考にしています。

jukenphysics.blog.fc2.com


一方、以下の記事では、 z \inftyまでの面積(確率)を求めている標準正規分布表を紹介しています。

staff.aist.go.jp

もし、統計検定の試験や大学の統計学の試験などで、普段と見慣れない標準正規分布表が出てきたとしても、本質としては変わらないので焦らずに求めるようにしましょう。

社会に出たら統計解析向けのR言語のライブラリやPythonのライブラリが求めてくれるので、あんまり気にしなくていいと思います。(とは言っても、どうやって見れば良いかを知っておくのは大事ですよ。)

統計学基礎vol.33〜母平均の信頼区間を求める(母分散が既知の場合)〜

今回の記事は母分散が既知の場合の、母平均の信頼区間の値を求めてみます。

正直な話、母平均が未知なのに、どうして母分散が分かるんだって気がしますが...。そういう特殊ケースがあるんでしょう、世の中には。

母平均の信頼区間~母分散が既知の場合~

手順として大きく4つになります。また、今回は 95\%信頼区間を例に見ていくことにしますが、信頼係数が 90\%でも 99\%でもやることは変わりません(途中の数値が変わるぐらいです)。

  1. 標本平均 \bar{x}を求める。
  2. 標本平均の標準化を行う。
  3. 2で標準化した値が標準正規分布 95\%の面積の範囲にあれば良いので、標準正規分布表から上側 2.5\%点を求める。
  4. 信頼区間を求める。

では、順に見ていきましょう。

1. 標本平均 \bar{x}を求める

言葉の通りです。標本平均を求めます。仮に n個のデータ X = \{ x_1, x_2, \dots, x_n \} の標本平均を求める場合は、

 \displaystyle \bar{x} = \frac{1}{n} \sum_{i=1} ^{n} x_i

となります。

2. 標本平均の標準化を行う

母平均は未知なので \muとします。そして、(なぜか値が分かっている)母分散を \sigma ^2、サンプルサイズを nとすると(ここの nは1で出てきた nです)、標準化した値を zで表して、

 \displaystyle  z = \frac{\bar{x} - \mu}{\sqrt{\frac{\sigma ^2}{n}}}

となります。

3. 標準正規分布表から上側2.5%点を求める

例によって、標準正規分布表から求めます。
ここで、なぜ 95\%の信頼区間を求めるのに、 5\%ではなく 2.5\%なのかというと、上と下(正規分布で言えば、右端と左端)の 2.5\%を合わせると 5\%になりますよね。
このように、信頼区間上と下の両側を調べることを両側検定と言い、反対に信頼区間の上もしくは下の片側一方だけを調べることを片側検定と言います。

(両側検定、片側検定の詳しい話に関しては後々解説することにします。今すぐ知りたい方は統計Webさんの23-6. 両側検定と片側検定を参照ください。)

では、標準正規分布表から面積が 2.5\%となる Zの値を探してみましょう。
標準正規分布表では、0~ Zまでの確率(積分値)を求めています。一方で、私たちが求めたいのは下図の赤い矢印が指す部分です。
このとき、赤い矢印の部分の確率は 2.5\%なので、標準正規分布表では反対に 47.5\%となる zの値を求めればよいです。

f:id:kurasher:20210313224208p:plain

よって、標準正規分布表から 47.5\%となる zの値は

 z = 1.96

となります。図で表すと下の色のついた範囲が 2.5\%になります。

f:id:kurasher:20210313232509p:plain

式で表すと下記になります。

 \displaystyle \int_{1.96} ^{\infty} f(z) dz = 0.025

4. 信頼区間を求める

3から、上側 2.5\% zの値は 1.96(下側 2.5\% zの値は -1.96)と分かりました。ここで、母平均の 95\%の範囲を知りたいので、その範囲を zで表すと、

 -1.96 \leq z \leq 1.96

となります。

さて、ここで問題に立ち返ると、我々は 95\%信頼区間の母平均の値を知りたいです。そのため、上式を母平均 \muについて解かないといけません。
ここで、

 \displaystyle  z = \frac{\bar{x} - \mu}{\sqrt{\frac{\sigma ^2}{n}}}

としていたので、これを代入して \muについて解けば良さそうです。

 -1.96 \leq z \leq 1.96

 \displaystyle -1.96 \leq \frac{\bar{x} - \mu}{\sqrt{\frac{\sigma ^2}{n}}} \leq 1.96

 \displaystyle -1.96 \sqrt{\frac{\sigma ^2}{n}} \leq \bar{x} - \mu \leq 1.96\sqrt{\frac{\sigma ^2}{n}}

 \displaystyle \bar{x} -1.96 \sqrt{\frac{\sigma ^2}{n}} \leq \mu \leq \bar{x} + 1.96\sqrt{\frac{\sigma ^2}{n}}

 \muについて解くとこのようになりました。ここで、 \bar{x} \sigma ^2 nはすべて既知の値であるため、母平均 \muの値を求めることができます。

一般化する

今回、信頼係数が 95\%というもとで、信頼区間の求め方の手順を見てきました。ここで、最後の手順4で出てきた展開式を一般的に表してみることにします。

信頼係数を \alpha(0 \leq \alpha \leq 1) 100\alpha \%)とすると、標準正規分布の上側確率は

 \displaystyle \frac{1-\alpha}{2}

と表現できます。この時の zの値を z(\frac{1-\alpha}{2})で表すと、最後の手順4で出てきた展開式は以下のように表せます。

 \displaystyle \bar{x} - z\left(\frac{1-\alpha}{2} \right) \sqrt{\frac{\sigma ^2}{n}} \leq \mu \leq \bar{x} + z \left( \frac{1-\alpha}{2} \right) \sqrt{\frac{\sigma ^2}{n}}

 95\%信頼区間であれば、 z\left(\frac{1-\alpha}{2} \right) = 1.96となります。
ちなみに 99\%信頼区間であれば、 z\left(\frac{1-\alpha}{2} \right) = 2.58となります。

区間推定の特徴(母分散が既知)

信頼区間の幅は、
信頼係数 \alpha(0 \leq \alpha \leq 1)が小さいときほど
また
サンプルサイズ nが大きいほど
狭くなります。

まとめ

今回の記事のまとめです。

~母分散既知の母平均の信頼区間の手順~

  1. 標本平均 \bar{x}を求める。
  2. 標本平均の標準化を行う。
  3. 2で標準化した値が標準正規分布 100\alpha \%の面積の範囲にあれば良いので、標準正規分布表から上側 z(\frac{1-\alpha}{2})点を求める。
  4. 信頼区間を求める。

 \displaystyle \bar{x} - z\left(\frac{1-\alpha}{2} \right) \sqrt{\frac{\sigma ^2}{n}} \leq \mu \leq \bar{x} + z \left( \frac{1-\alpha}{2} \right) \sqrt{\frac{\sigma ^2}{n}}


~信頼区間の特徴~
信頼係数 \alpha(0 \leq \alpha \leq 1)が小さいほど、また、サンプルサイズ nが大きいほど狭くなる

統計学基礎vol.32~区間推定~

こんにちは

最近は、TwitterGAFA(米国の主要IT企業であるグーグル(Google)、アマゾン(Amazon)、フェイスブックFacebook)、アップル(Apple)の4社の総称)社員を名乗る方のつぶやきをみて鼻で笑っていますWindowsのショートカットキーを勉強しています。

まぁ流石にネタだと思いますけどね。

さて、こんな前置きをしてますが、今回の記事とは一切関係ありません

区間推定とは

母集団の従う分布が正規分布であると仮定できる時、標本から得られた値を使って、ある区間で持って母数を推定する方法。この時の区間信頼区間という。(論文では略記でCI。おそらくConfidence Intervalのこと。)

区間推定を常日頃から使っているような学生さんや、データ分析を主とする方々はよく、「〇〇%信頼区間」という言葉を聞いたことがあるかと思います。よく使われる数字は95%、99%、90%がほとんどだと思われます。

このような、ある区間に母数が含まれる確率(95%や99%など)のことを信頼係数(信頼度)と言います。

95%信頼区間が意味するもの

ここで、仮に母平均を95%信頼区間で推定するとします。
統計Web 19-3. 95%信頼区間のもつ意味の記事でも紹介されていますが、95%信頼区間では95%の確率でその範囲にあるということを表しています

これは、①「正規分布に従う母集団から標本を取ってきてその平均から95%信頼区間を求めた時に、その区間の中に95%の確率で母平均が含まれる」という意味ではありません。

正しくは、②「母集団から標本を取ってきて、その平均から95%信頼区間を求める、という作業を100回やったときに、95回はその区間の中に母平均が含まれる」です。

①と②の違いは何かというと、①は母集団から標本をとる作業を1回しか行っていませんが、②は母集団から標本をとる作業を100回行っています。このうち、95回は指定した区間内に母平均が含まれることが言えます。

①と②、違いが微妙ではありますが、使い方には十分気をつけましょう。

まとめ

今回の記事のまとめです。

次回は、母分散が既知の場合の母平均の信頼区間の求め方についてまとめます。

言葉 意味
区間推定 母集団の従う分布が正規分布であると仮定できる時、標本から得られた値を使って、ある区間で持って母数を推定する方法
信頼係数(信頼度) ある区間に母数が含まれる確率(95%や99%など)のこと

あなたの身長は上位何%?(男性版のみ)

最近暇を持て余しておりまして。(嘘です。任された仕事がいつまで経っても終わらず、先輩方に迷惑ばかりかけてないか不安です。)

そんなおり、友人にけしかけられたということもあり、マッチングアプリなるものをはじめてみました。

いろいろな女性がいらっしゃるわけですが、ある時、ある女性が求める条件の一つに「高身長男性」という項目がありました。

どうやらその女性は自らの身長が高いということもあり、男性に求める最低身長が180cm超えでした。

高いなと思いつつ、180cm超えってそんなにいるっけ?と思い、日本の成人男性の上位何%が180cm超えているのか気になり調べてみました。

データ

データは以下のサイトから、Excelデータを取得してきました。

e-Stat 政府統計の総合窓口
令和元年国民健康・栄養調査

今回調べたいのは日本の成人男性(20歳以上)の平均身長と標準偏差なので、他の項目は無視します。

この時、20歳以上の成人男性の人数は 1,968人、平均身長は 167.7cm標準偏差 6.9 cmです。

データの標準化

上記のデータの値をそのまま使うのもありですが、せっかくなので標準化してみます。

標準化とは、与えられたデータを平均が0、分散が1のデータに変換する操作のことです。標準変数を zとし、各データを x_i、平均値を \mu標準偏差 \sigmaとすると、

 \displaystyle z = \frac{x_i - \mu}{\sigma}

となります。この時、成人男性の身長という X軸を、標準化した Z軸で表すと、

 \displaystyle z = \frac{180.0 - 167.7 }{6.9} = 1.782608696 \approx 1.78

とできます。上記より、 x = 180という値は、標準変換すると Z軸上では z \approx 1.78で表せることがわかりました。

身長が180cm以上の男性の割合は?

ここから標準正規分布表を使って、 0 \leq z \leq 1.78がどのくらいかを調べてみます。

標準正規分布表を参考に値を見つけると、

 \displaystyle P(Z \leq 1.78) = 0.4625


よって、平均身長 167.7cmから 180cmまでの成人男性の割合はおおよそ 46.25\%ということがわかりました。

つまり、成人男性の身長が 180cm以下の人は、(平均身長 167.7cm以下の人も含めて) 96.25\%ということがわかりました。


では、逆に身長 180cm以上の人はどれくらいいるのかというと、全体から身長が 180cm以下の人の割合を引けば良いので、

 100\% - 96.25\% = 3.75\%

よって、日本の成人男性の上位 3.75\%の人が180cm超えているということがわかりました。


なるほど。 これを多いとみるか、少ないと見るかはあなた次第になりそうです。

もちろん、このデータは日本人だけなので、ガタイの良い外国人の方も含めると全く違う結果になると思います。

サンプルコード

暇なのでサンプルコードを作ってみました。調べたい身長の部分を変えるといろいろ調べることが可能です。
環境はGoogle Colaboratoryです。

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import quad
from scipy.stats import norm

MEAN = 167.7
SIGMA = 6.9
input = 180 #調べたい身長

Z = (input - MEAN) / SIGMA
I = quad(lambda x: 1/(np.sqrt(2 * np.pi)) * np.exp( -1 * x**2 / 2), 0, Z)
print("あなたの身長は上位{:}%".format((1.0 - (I[0] + 0.50)) * 100))

x = np.linspace(-3.5, 3.5, 101) 
y = np.exp(-x**2 / 2) / np.sqrt(2 * np.pi)
plt.plot(x, y)

x1 = np.linspace(Z,3.5,101)
y1 = np.exp(-x1**2 / 2) / np.sqrt(2 * np.pi)
plt.fill_between(x1, y1, where=y1>0,facecolor='y')
plt.show()

実行すると以下のグラフが出てきます。

f:id:kurasher:20210221142825p:plain

 180cmを超える成人男性は色のついた部分のみになります。いずれにせよ、視覚化すると分かりやすいですね。