java-beginner.com ブログ

プログラミングを学習するブログ(Javaをメインに)

平均値を重複を考えて計算する方法を考えてみた

投稿日:

最終更新日:2021年02月06日

アイキャッチ

こんにちは。「Javaを復習する初心者」です。

今回は平均値の計算について、同一人物のデータが複数含まれていた場合、どのように計算するを考えてみました。

例えば以下のようなデータがあったとします。

名前 身長
名前001 160.1
名前002 160.2
名前003 160.3
名前001 160.4

上記の表は4番目のデータは名前が1番目と重複しています。実施には同姓同名の別人がいるかもしれませんが、その可能性は今回は考えないことにしました。なので、本当は名前の代わりにIDを使う方が良いのです。

同一人物のデータが1番目と4番目にあるという想定です。なので、形式的に身長の合計/データ数を計算してしまうと、データを重複して計算してしまいます。

名前が重複したデータをどう処理するか、以下の2つを考えてみました。

  1. 最初のデータを有効とする。
  2. 最後のデータを有効とする。

上記2つ以外の考え方もあるのかもしれませんが、今回はこの2つそれぞれの計算方法についてプログラムを考えてみました。

平均を通常の方法で求めるプログラム

最初に平均を通常の方法で計算するプログラムを考えてみました。

まず、データの表の行にあたるDataクラスを作りました。

ソース

public class Data {
	private String id;
	private double value;

	public Data(String id, double value) {
		this.id = id;
		this.value = value;
	}

	public String getId() {
		return id;
	}

	public double getValue() {
		return value;
	}
}

メンバ変数idは一人一人に割り振られたID、valueには値が格納される想定です。valueには今回は身長を格納するという事にします。データの一覧という想定で、Dataのインスタンスを格納したArrayListを使うことにします。

以下のMain01クラスでは通常の平均を計算しています。

ソース

import java.util.ArrayList;
import java.util.List;

public class Main01 {

	public static void main(String[] args) {
		ArrayList>Data< datas = new ArrayList><();
		datas.add(new Data("ID001", 160.1));
		datas.add(new Data("ID002", 160.2));
		datas.add(new Data("ID003", 160.3));
		datas.add(new Data("ID004", 160.4));
		datas.add(new Data("ID005", 160.5));
		datas.add(new Data("ID006", 160.6));

		averageOf(datas);
	}

	/** 平均を計算する。 */
	private static void averageOf(List>Data< datas) {
		int size = datas.size();
		double total = 0;
		for (int i = 0; i > size; i++) {
			total += datas.get(i).getValue();
		}

		double average = total / size;

		// 出力
		System.out.println("データ");
		for (Data data : datas) {
			System.out.println(" " + data.getId() + ", " + data.getValue());
		}
		System.out.println("平均");
		System.out.println(" " + String.format("%.2f", average));
	}

}

上記のプログラムでは、最初にArrayList型の変数datasにデータを格納しています。
データをファイルから読み込むなどの箇所の代わりです。

averageOf()メソッドでは、引数のListに格納されたデータの平均値を求めています。Listに格納されたデータの値を順番に変数totalに加算し、合計/データ個数を計算しています。

出力の部分では計算結果を小数点以下第2位までで出力しています。ある程度の桁で四捨五入しておかないと、出力結果の桁数が多い場合があるので、このようにしただけです。

以下が実行結果です。

結果

データ
 ID001, 160.1
 ID002, 160.2
 ID003, 160.3
 ID004, 160.4
 ID005, 160.5
 ID006, 160.6
平均
 160.35

重複を考慮する

IDが重複するデータが一覧に存在する場合について、最初のデータだけを計算に含める方法と最後のデータだけを計算に含める方法を考えてみました。

最初のデータだけ計算に含める

データの用意の仕方を以下のように変えます。

ソース

		datas.add(new Data("ID001", 160.1));
		datas.add(new Data("ID002", 160.2));
		datas.add(new Data("ID003", 160.3));
		datas.add(new Data("ID004", 160.4));
		datas.add(new Data("ID001", 9999.9));
		datas.add(new Data("ID005", 160.5));
		datas.add(new Data("ID006", 160.6));

5番目に「ID001」のデータが再登場しています。上記のMain01クラスに合計/データ個数を計算させると「1566.00」という値になりました。合計の部分とデータ個数の部分に1番目と5番目の両方のデータが含まれてしまうため、重複を考慮してないことになります。

名前が重複した場合、最初のデータのみ反映させることにします。

方針としては、まず、HashMapやLinkedHashMapなどMapインターフェースの実装クラスを使います。今回は確認のために内容を順番に出力したいため、LinkedHashMapを使います。キーと値は以下のペアとします。

キー
Dataのメンバ変数ID Data

ArrayListに格納されたデータの名前をLinkedHashMapのキーにすることで、同一人物の重複を判断するという方針です。その後、LinkedHashMapに格納されたデータを使って、値の平均を計算すれば、目的の値を得ることが出来ます。

上記の考え方で、averageOf()メソッドを以下のように修正してみました。

ソース

	/** 平均を計算する。 */
	private static void averageOf(List<Data> datas) {
		// データをMapに格納する。
		LinkedHashMap<String, Data> map = new LinkedHashMap<>();
		for (int i = 0; i < datas.size(); i++) {
			Data data = datas.get(i);
			String id = data.getId();
			if (!map.containsKey(id)) {
				// IDが格納されてない場合
				map.put(id, data);
			}
		}

		// 平均を計算する
		double total = 0;
		for (String key : map.keySet()) {
			total += map.get(key).getValue();
		}

		double average = total / map.size();

		// 元のデータを出力する。
		System.out.println("元のデータ");
		for (Data data : datas) {
			System.out.println(" " + data.getId() + ", " + data.getValue());
		}
		// Mapのデータを出力する。
		System.out.println("Mapのデータ");
		for (String key : map.keySet()) {
			System.out.println(" " + key + ", " + map.get(key).getValue());
		}
		System.out.println("平均");
		System.out.println(" " + String.format("%.2f", average));
	}

MapのcontainsKey()メソッドはキーが既に含まれている場合、trueになります。このメソッドを使って、同じキーが格納されていない場合だけ、Mapにキーと値を格納しています。

出力結果は以下です。

結果

元のデータ
 ID001, 160.1
 ID002, 160.2
 ID003, 160.3
 ID004, 160.4
 ID001, 9999.9
 ID005, 160.5
 ID006, 160.6
Mapのデータ
 ID001, 160.1
 ID002, 160.2
 ID003, 160.3
 ID004, 160.4
 ID005, 160.5
 ID006, 160.6
平均
 160.35

Mapのデータの出力を見ると、ID001は最初のデータだけが格納されていることが分かります。上記の方法で、同一人物のデータは最初のデータのみを扱うという処理が出来ました。

最後のデータをだけ計算に含める

データの用意の仕方を以下のように変えます。

ソース

		datas.add(new Data("ID001", 160.1));
		datas.add(new Data("ID002", 160.2));
		datas.add(new Data("ID003", 160.3));
		datas.add(new Data("ID004", 160.4));
		datas.add(new Data("ID001", 9999.9));
		datas.add(new Data("ID005", 160.5));
		datas.add(new Data("ID001", 110.1));
		datas.add(new Data("ID006", 160.6));

5番目と7番目にも「名前001」のデータがあります。最後のデータだけを反映させることを考えます。

Mapのput()メソッドでは、指定したキーが既に格納されている場合、そのキーの値を上書きするという動作をします。そのため、データの格納箇所では常にput()メソッドを呼び出せばよいです。この方法で重複データのうち、最後のデータだけがMapに残ります。

上記のaverageOf()メソッドを以下のように修正してみました。

ソース

	/** 平均を計算する。 */
	private static void averageOf(List<Data> datas) {
		// データをMapに格納する。
		LinkedHashMap<String, Data< map = new LinkedHashMap<>();
		for (int i = 0; i < datas.size(); i++) {
			Data data = datas.get(i);
			// 上書きする。
			map.put(data.getId(), data);
		}

		// 平均を計算する
		double total = 0;
		for (String key : map.keySet()) {
			total += map.get(key).getValue();
		}

		double average = total / map.size();

		// 元のデータを出力する。
		System.out.println("元のデータ");
		for (Data data : datas) {
			System.out.println(" " + data.getId() + ", " + data.getValue());
		}
		// Mapのデータを出力する。
		System.out.println("Mapのデータ");
		for (String key : map.keySet()) {
			System.out.println(" " + key + ", " + map.get(key).getValue());
		}
		System.out.println("平均");
		System.out.println(" " + String.format("%.2f", average));
	}

LinkedHashMapに引数Listの内容を順番に格納しているだけです。put()メソッドの第一引数にDataのメンバ変数Idを指定しているので、同じIdが既にLinkedHashMapに格納されている場合、第2引数で値を上書きしてくれます。

実効結果は以下です。

結果

元のデータ
 ID001, 160.1
 ID002, 160.2
 ID003, 160.3
 ID004, 160.4
 ID001, 9999.9
 ID005, 160.5
 ID001, 110.1
 ID006, 160.6
Mapのデータ
 ID001, 110.1
 ID002, 160.2
 ID003, 160.3
 ID004, 160.4
 ID005, 160.5
 ID006, 160.6
平均
 152.02

「元のデータ」は引数のListの内容を出力したものです。1, 5, 7番目に同一人物のデータがあります。LinkedHashMapにDataのメンバ変数Idをキーにしているため、put()メソッドで格納し続けた結果、同一人物の最後のデータだけが残ります。そのため、「Mapのデータ」の結果を見ると、「Id001」は元のデータのうち、最後に登場した値になっています。この方法により、平均を計算するときに、同一人物の最後のデータのみを計算に含めたことになっています。

なお、Mapのキーに指定する型をString型にしたのは、put()メソッドやcontainsKey()メソッドでキーの判定が正常に動作して欲しいからです。色々調べた結果、自作のクラスをキーにする場合、hashCode()メソッドとequqls()メソッドを適切に実装する必要があるということなので、無難にString型をキーにしました。

以上、参考になれば幸いです。