アルゴリズムの基本をJava、C#、Pythonで学ぼう - データを集計し、言語ごとの違いを知る
基本的なアルゴリズムを使った観測データの集計を考えてみます。使用するのは、Java(Java 13)、C#(C# 8.0)、Python(Python3)。ちょっとしたコードでも言語によって違いがあります。
最近は、便利なライブラリやAPIが多数公開されており、自分でアルゴリズムを考えるのは非効率と思えるほどです。とはいえ、基本のアルゴリズムや定番的な処理方法を押さえていないと、生産性やプログラムの品質に影響します。
この記事では、基本的なアルゴリズムを使った観測データの集計を考えてみます。使用するのは、人気言語のJava(Java 13)、C#(C# 8.0)、Python(Python3)。ちょっとしたコードでも言語の違いがありますので、言語選定などの参考にしてみてはいかがでしょう。
アメダスの観測データを扱う
今回扱うデータは、気象庁が提供しているアメダスの観測データです。アメダスは、「Automated Meteorological Data Acquisition System」の略で、降水量や風向・風速、気温などを自動的に観測している気象庁のシステムです。
アメダスの観測データは、気象庁のWebサイトから、CSV形式のファイルとしてダウンロード可能です。地点、項目、期間などを指定して、ダウンロードします。
▲ 「過去の気象データ・ダウンロード」のページ
今回は、以下のように指定しました。表示オプションはデフォルトのままです。
- 地点を選ぶ:東京
- 項目を選ぶ(データの種類):時別値
- 項目を選ぶ(項目):気温
- 期間を選ぶ:2019/1/1~2019/12/6
データの項目は「日時」「気温」「品質情報」「均質番号」の4種類。品質情報、均質番号は、観測値が正常かどうかに関する情報です。各項目の詳細については、同サイトの「ダウンロードファイル(CSVファイル)の形式」ページを参照してください。
ファイルの先頭5行には、次の画像のような項目が入っています。このまま使うとやや面倒なので、あらかじめ先頭5行分は削除し、全てデータ行のみのCSVファイルに加工しておきます。
▲ CSVファイルのデータ
CSVファイルの仕様にはバリエーションがあり、その全てに対応するとコードが複雑になりますので、今回は気象庁のCSVデータに基づき、次の仕様を前提とします。
- 文字種は、ANK(1バイト文字)のみ
- ヘッダ行なし
- 区切り文字にはカンマを使う
- 値は、ダブルクォートで囲まない(値にカンマや改行は含まない)
観測データの集計
観測された気温データを集計し、次の項目を求めてみます。
- 最高気温、最低気温と、その日時
- 月ごとの最高気温、最低気温、平均気温
このような集計は、実際のシステムでは、データベースを使うのが一般的でしょう。データベースを使えば比較的簡単に結果を得られます。ただ今回は、データベースを使わない(使えない)という前提で、この集計を行うプログラムを考えることにします。
処理の流れ
まずは、ざっと処理の流れを考えてみましょう。少なくとも、次のような処理が必要そうです。
- CSVファイルの読み込み
- データの抽出
- 集計処理
- 結果の表示
CSVファイルの読み込み
ひとくちにCSVファイルの読み込みといっても、言語によっていくつか方法があります。今回は、最も基本的と思われる手法を紹介することにします。
一般に、ファイルを処理するには、大きく2つの方法があります。一つは「ファイルをいったん全てメモリに読み込んでから後の処理を行う方法」、もう一つは「読み込みながら処理を行う方法」です。
ファイルサイズやデータ行の大きさ、実行する環境のメモリ容量、実行速度、さらに処理内容によって、どちらの方法を取るか考える必要があります。今回は、巨大なファイルにも対応できるように「読み込みながら処理を行う方法」を採用します。
集計処理
まず、最高気温、最低気温の日時を求める方法です。最高気温、最低気温の日時がそれぞれ1つなら、気温を基準に順番にならべて(ソート)、最初と最後の日時を表示すれば済みます。ただ実際には、同じ最高気温、最低気温の日時があり得ます。今回は、ソートを使わない方法で実装してみました。
なお、日の気温データは、1時~24時(翌日0時)の値を集計する必要があるのですが、今回は、0時~23時の値を1日分として扱います。
結果の表示
今回の集計処理の結果は、次のような内容をコンソールに表示することにします。最初に、最高気温とその日時。次に最低気温とその日時、そして、月ごとの最高気温、最低気温、平均気温です。
最高:35.3℃ 2019/8/8 13:00 最低:-0.7℃ 2019/1/10 6:00 1月 最高:13.7℃ 最低:-0.7℃ 平均:5.6℃ 2月 最高:18.7℃ 最低:-0.6℃ 平均:7.2℃ 3月 最高:21.9℃ 最低:1.9℃ 平均:10.6℃ 4月 最高:25.0℃ 最低:2.5℃ 平均:13.6℃ 5月 最高:31.9℃ 最低:8.5℃ 平均:20.0℃ 6月 最高:32.0℃ 最低:14.2℃ 平均:21.8℃ 7月 最高:33.8℃ 最低:18.0℃ 平均:24.1℃ 8月 最高:35.3℃ 最低:20.9℃ 平均:28.4℃ 9月 最高:35.0℃ 最低:17.4℃ 平均:25.1℃ 10月 最高:29.9℃ 最低:12.3℃ 平均:19.4℃ 11月 最高:22.9℃ 最低:2.1℃ 平均:13.1℃ 12月 最高:16.1℃ 最低:4.8℃ 平均:9.4℃
Javaでのプログラム
Javaを用いてCSVなどのテキストファイルを処理する方法は複数あり、どれを利用すればいいのか迷うほどです。ただ、Java7以降であれば、Filesクラスを使うのが最も便利でしょう。FilesクラスのnewBufferedReaderメソッド(BufferedReaderオブジェクトの生成)を使えば、テキストファイルの読み込みがシンプルに記述できます。
最高気温、最低気温の日時を求める
まず、全体の処理から説明しましょう。なお、記事本文にソースコードを全て掲載すると長くなるので、以下のリンクから別途ダウンロードの上、確認してください。
var dtmp = new Dtemp(); // 最高・最低気温とその日時を処理するクラスのインスタンス生成 var file = Paths.get("data.csv"); // CSVファイルの指定 try (var br = Files.newBufferedReader(file);) { // ファイルのオープン 【1】 String line = null; while ((line = br.readLine()) != null) { // 1行ずつ取得 【2】 var r = line.split(","); // カンマで分割する 【3】 if (r[2].compareTo("8") == 0) { // 品質情報が8のみ対象にする 【4】 dtmp.setValue(r[0], Float.valueOf(r[1])); // 気温の比較処理 【5】 } } } catch (IOException e1) { e1.printStackTrace(); } // 結果の表示を行う System.out.printf("最高:%.1f℃\n", dtmp.smax); dtmp.max_days.forEach(System.out::println); System.out.printf("最低:%.1f℃\n", dtmp.smin); dtmp.min_days.forEach(System.out::println);
ファイルオープン処理では、try-with-resources文を用いています(【1】)。この構文を使うと、文の終わりでクローズ処理が自動的に実行されます。そのため、クローズ処理の記述漏れなどのミスを防げます。
BufferedReaderクラスのreadLineメソッドを使うと、CSVファイルから1行ずつ文字列として取得できます(【2】)。このwhileループではreadLineメソッドがnullを返すまで、繰り返し実行されます。1行分の文字列は、splitメソッドを使って、カンマごとの配列に分割します(【3】)。
品質情報の項目が8のときだけが正常の気温になっているので、その時だけの値を対象にします(【4】)。
最高気温、最低気温とその日時を保存するための処理は、次のようなクラスとして定義しました。
// 最高・最低気温とその日時を処理するクラス class Dtemp{ public float smax = -999, smin = 999; // 最高気温、最低気温の保存用 【6】 public List<String> max_days = new ArrayList<>(); // 最高気温の日時 public List<String> min_days = new ArrayList<>(); // 最低気温の日時 // 気温の比較処理 【5】 // 引数は、日時とそのときの気温 public void compValue(String dstr, float tmp) { if (max_days.isEmpty() || smax <= tmp) { // 保存した値より大きい場合 if (smax < tmp) max_days.clear(); // 保存した文字列を削除 【7】 smax = tmp; max_days.add(dstr); } if (min_days.isEmpty() || tmp <= smin) { // 保存した値より小さい場合 if (tmp < smin) min_days.clear(); // 保存した文字列を削除 【7】 smin = tmp; min_days.add(dstr); } } }
最大値、最小値を求めるアルゴリズムは、ごく基本的なものです。
まず、初期値として暫定で最小値と最大値を決めます(【6】)。そして、順番にデータの全ての値と比較していきます(【5】)。より大きい値、小さい値が見つかれば、その値を保存したものと入れ替えていきます。このようにすると、最後には全体の最大値、最大値が保存されることになります。
最初の最大と最小の暫定値は、このコードのように、データ内の値より小さい値と大きい値(-999、999)にする、あるいは空(NULL)とする方法や、データの最初の値を用いる方法などがあります。最初の値を基準に用いるのは、テレビのコンクール番組(M-1グランプリなど)でもおなじみの手法ですね。
なお最高気温、最低気温は一つだけですが、日時は複数の場合があるので、ここでは、ArrayListクラス(可変の配列)を使って、複数の文字列を格納できるようにしています。保存した気温が更新されたときは、保存した日時を削除して、空にしています(【7】)。
最高気温、最低気温の日時を求めるアルゴリズムは、他にもいろいろと思い付くでしょう。前述したソートも、例えば最高・最低気温と日時を見つける処理とを分ければ、利用可能です。先に最高気温、最低気温だけをソートして求め、次に、その気温の日時を探索する方法です。データを全てメモリ上に読み込めるのなら、この方法の方が処理は速くなるかもしれません。
月ごとの集計
月ごとの集計でも、基本的なところは同じです。気温を比較して保存するクラスは、次のようにしました。
// ひと月の最高・最低気温を処理するクラス class Mtemp{ public float smax = -999, smin = 999; public float sum = 0; // ひと月の気温の合計 public int ct = 0; // ひと月の気温値の数 // 気温の比較処理 【1】 // 引数は、気温 public void compValue(float tmp) { if (smax < tmp) smax = tmp; if (tmp < smin) smin = tmp; sum += tmp; ct++; } }
compValueメソッドでは、最高最低気温だけでなく、平均を計算するために、気温の合計とその個数も保存するようにしています(【1】)。
気温の合計とその個数を求めるアルゴリズムも、ごくシンプルなものです。それぞれ初期値を0にしておき、データのループ処理のなかで、気温を加算、初期値をインクリメントする方法です。
なお、「月単位の気温」というデータを保持する必要があるので、あらかじめ12個のオブジェクトを生成します。CSVファイルの処理の前に、HashMapクラスを使って、月をキーに、Mtempオブジェクトを値として初期化します。なお、キーと値のペアを保持するデータ構造を、ハッシュテーブルと呼びます。
// 12ヶ月分のオブジェクトを作成しておく var map = new HashMap<Integer, Mtemp>(); for(int i=1; i<=12; i++) { map.put(i, new Mtemp()); }
CSVファイルのwhileループの処理では、次のように実装しました。
// 気温の比較 // 日時文字列から、月のみを取り出す var month = LocalDate.parse(r[0], DateTimeFormatter.ofPattern("yyyy/M/d H:mm")).getMonthValue(); // 【2】 map.get(month).compValue(Float.valueOf(r[1])); // 【3】
まず日時文字列から、月のみを取り出します。月を抽出する処理はいくつかあります。ここでは、日時文字列から、LocalDateクラスを使って、年月日の情報に変換し、getMonthValueメソッドで、月のみを参照しています。
そして、HashMapオブジェクトから、該当する月のMtempオブジェクトを参照して、compValueメソッドを実行しています(【3】)。
結果の表示では、HashMapオブジェクトからentrySetメソッドを使って、キーと値を順番に参照しています。
// 結果の表示 for (var m : map.entrySet()) { System.out.printf("%2d月 最高:%.1f℃ 最低:%.1f℃ 平均:%.1f℃\n", m.getKey(), m.getValue().smax, m.getValue().smin, m.getValue().sum/m.getValue().ct); }
C#でのプログラム
C#でも、Javaと同様のアルゴリズムで実装でき、コード自体はかなり似たものになります。ただC#の方が、少しシンプルに記述できます。Javaと異なる箇所に絞って説明することにしましょう。
CSVファイルなどのテキストファイルの読み込みは、StreamReaderクラスを利用します。ReadLineメソッドで、1行ずつ取得できます。C#では、usingステートメントか、変数にusingを付けると(C#8以降)、オブジェクトの破棄が自動で実行されます。
using var sr = new StreamReader(@"data.csv"); while (!sr.EndOfStream) { var line = sr.ReadLine(); // 1行ずつ取得 ~略~
Dtemp、Mtempクラスでは、ほとんど同じコードとなりますが、ArrayListの代わりに、Listクラスとなります。また、メソッド名は、大文字から始める、いわゆるPascal表記が推奨されています。
最高気温、最低気温の日時を求める
// 最高・最低気温とその日時を処理するクラス class Dtemp { public float smax = -999, smin = 999;; // 最高気温、最低気温の保存用 public readonly List<string> max_days = new List<string>(); // 最高気温の日時 public readonly List<string> min_days = new List<string>(); // 最低気温の日時 // 気温の比較処理 // 引数は、日時とそのときの気温 public void CompValue(string dstr, float tmp) ~略~
月ごとの集計
日時文字列から、月のみを取り出す処理は、DateTimeクラスのParseメソッドを使いますが、標準的な書式であれば、特に書式を指定していなくても、日時を解析できます。
C#では、Dictionaryクラスがキーと値のペアを保持するハッシュテーブルのクラスとなります。
var month = DateTime.Parse(r[0]).Month; // 日時文字列から、月のみを取り出す // 12ヶ月分のオブジェクトを作成しておく var map = new Dictionary<int, Mtemp>();
結果の表示
結果の表示のところ、C#6.0以降では、文字列の先頭に$を付けると、変数を{}で囲んで埋め込むことが可能です。また、書式指定子により、小数点の桁数などを指定することもできます。
// 月単位で最高気温、最低気温、平均気温の表示 foreach (var m in map) { Console.WriteLine($"{m.Key,2}月 最高:{m.Value.smax:F1}℃ 最低:{m.Value.smin:F1}℃ 平均:{(m.Value.sum / m.Value.ct):F1}℃"); }
JavaとC#は、共にC/C++言語をベースとした構文のため、コードの見た目や文法的な違いは、あまり感じないかもしれません。しかし細部を見ていくと、いろいろと異なっています。
Pythonでのプログラム
Pythonでも、もちろんJavaと同様のアルゴリズムで実装可能ですが、コードはよりシンプルに記述できます。
CSVファイルの読み込みは、標準ライブラリのcsvモジュールを使うと、簡単に読み書き可能です。またPythonでは、with構文が、try-with-resources文、using変数宣言と同等の処理になります。
最高気温、最低気温の日時を求める・月ごとの集計
try: # CSVファイルのオープン with open('data.csv') as csvfile: for r in csv.reader(csvfile): # 1行ずつ取得 if r[2] == "8": # 品質情報が8のみ対象にする dtmp.compValue(r[0], float(r[1])) # 気温の比較処理 # 日時文字列から、月のみを取り出す month = datetime.datetime.strptime(r[0], '%Y/%m/%d %H:%M').month map[month].compValue(float(r[1])) # 気温の比較処理 # 結果の表示を行う ~中略~ except: print('file error')
Pythonの日時処理は、標準ライブラリのdatetimeモジュールを用います。月のみを取り出す処理は、datetime.datetime型のstrptimeメソッドを利用しています。strptimeメソッドでは、Java同様に、日時の書式を指定します。
Dtemp、Mtempクラスの定義と、12ヶ月分のMtempオブジェクトの生成は、次のようになります。Pythonのクラスでは、標準でメンバ全てがpublicとなります。またインスタンス変数の定義は、インスタンスの初期化を行うinitメソッド内に記述します。
Pythonでのハッシュテーブルは、マップ型となります。マップ型は組み込み型のため、{}で簡単に初期化できます。
# 最高・最低気温とその日時を処理するクラス class Dtemp: def __init__(self): self.smax = -999.0 # 最高気温、最低気温の保存用 self.smin = 999.0 self.max_days = [] # 最高気温の日時 self.min_days = [] # 最低気温の日時 # 気温の比較処理 # 引数は、日時とそのときの気温 def compValue(self,dstr,tmp): if self.max_days.count == 0 or self.smax <= tmp: # 保存した値より大きい場合 if self.smax < tmp: self.max_days.clear() # 保存した文字列を削除 self.smax = tmp self.max_days.append(r[0]) if self.min_days.count == 0 or tmp <= self.smin: # 保存した値より小さい場合 if tmp < self.smin: self.min_days.clear() # 保存した文字列を削除 self.smin = tmp self.min_days.append(r[0]) # ひと月の最高・最低気温を処理するクラス class Mtemp: def __init__(self): self.smax = -999.0 self.smin = 999.0 self.sum = 0.0 # ひと月の気温の合計 self.ct = 0 # ひと月の気温値の数 # 気温の比較処理 # 引数は、気温 def compValue(self, tmp): if self.smax < tmp: self.smax = tmp if tmp < self.smin: self.smin = tmp self.sum += tmp self.ct += 1 # 12ヶ月分のオブジェクトを作成しておく map = {} for i in range(1, 13): map[i] = Mtemp()
結果の表示
結果を表示する際は、Pythonでも、変数の埋め込みや書式を指定できます。
for k,v in map.items(): print(f'{k:2}月 最高:{v.smax:.1f}℃ 最低:{v.smin:.1f}℃ 平均:{(v.sum/v.ct):.1f}℃')
Pythonでは、インデントで文をグループ化しており、JavaやC#のように{}がないため、見た目はかなり異なった印象になります。また、標準ライブラリやオープンソースのライブラリが非常に充実しているので、コード量も少なく済むことが多いです。
最後に
最大、最小を見つけるアルゴリズムは、本当に単純なアルゴリズムですが、プログラムの基礎となる処理といえます。ソースの解説は、スペースの都合上ポイントのみの説明になってしまいましたので、興味があればぜひ全体のコードをダウンロードして、試してみてください。
この記事の内容が、皆さんの参考になれば幸いです。
高江 賢 (たかえ・けん)
監修:WINGSプロジェクト 山田 祥寛
WINGS @yyamada WINGSプロジェクト