roy > naoya > 基礎プログラミングII > (4)ハッシュ[2]

(4) ハッシュ[2]

[1]ハッシュの利点と問題点

ハッシュは配列のインデックスとは異なり、keyに任意の値を与えることができる。また、単一のkeyに対して複数のvalueを結びつけて保存することが可能であり、配列に比べ取り扱う変数の数を減らすことができるという利点がある。

しかし、配列がインデックスを使用することで代入した順番に値を表示できるのに対し、ハッシュではfor-endなどを使って代入された値を全て表示すると順番がばらばらになってしまう。前回の授業でも多少触れたが、次のプログラムでもう一度確認しよう。

これは、ある大学で行われたソフトボール大会の結果である。6チームで136試合を行い、最も勝率が高いチームが優勝となる(softball.txt)。

チーム            勝    負    引
アンツ            78    53     5
スネイルズ        52    82     2
ドラゴンフライズ  77    51     8
モスキートーズ    65    65     6
クリケッツ        50    83     3
ブックウォームス  75    52     9

チーム名をkey、勝数、負数、引分数をvalueとしてハッシュ変数softに代入し、勝率を求めて結果を表示するプログラムを書いてみると以下のようになる(softball.rb)。

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

require 'kconv'

soft = Hash.new

while line = gets
  if /(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/ =~ line
    # 1個目の() (\S+)→チーム名が入る
    # 2個目の() (\d+)→勝ち数が入る
    # 3個目の() (\d+)→負け数が入る
    # 4個目の() (\d+)→引分数が入る
    soft[$1] = [$2.to_f,$3.to_f,$4] # 配列を代入
  end
end

print"--チーム名------------+-勝ち-+-負け-+-引分--+--勝率---\n"
for team, data in soft
  # data には、[勝ち数, 負け数, 引分数] という配列が入っている

  printf("%s %d %d %d %f\n",team, data[0], data[1], data[2], data[0]/(data[0]+data[1]))

#  result = sprintf("%s %d %d %d %f\n",
#           team.toeuc.force_encoding("binary"),data[0], data[1], 
#           data[2], data[0]/(data[0]+data[1])).toutf8

#  puts result
  
  # dataの第0要素が勝ち数、第1要素が負け数、第2要素が引分数
end
sime{c1xxxxx}% chmod +x softball.rb[Return]
sime{c1xxxxx}% ./softball.rb softball.txt[Return]
--チーム名------------+-勝ち-+-負け-+-引分--+--勝率---
アンツ                    78     53      5     0.595
スネイルズ                52     82      2     0.388
ドラゴンフライズ          77     51      8     0.602
モスキートーズ            65     65      6     0.500
クリケッツ                50     83      3     0.376
ブックウォームス          75     52      9     0.591

結果を表示すると確かに勝率が追加されている。しかし、通常勝敗を示す場合は勝率が高い順に表示したい。今日は、ハッシュを用いた際に結果を昇順(小さい順)や降順(大きい順)に並び替えて表示する方法について確認していこう。

日本語文字列の桁ぞろえ方法

printfの書式制御文字の%sに対し、%10sのように桁ぞろえの数字を入れた際、現行のRuby1.9とそれ以前のバージョンでは挙動が異なる。

  • Ruby1.9: 日本語1文字を1桁とカウント
  • Ruby1.9以前:日本語1文字を2桁とカウント

例えば%-8sにすると、マイナスは左詰めを意味するので、右側にスペースを入れて、全体として8桁で表示する。「アンツ」「スネイルズ」を桁ぞろえすると次のような結果になる(Xが空白を表わす)。

アンツXXXXX
スネイルズXXX

日本語文字列が1文字としてカウントされるため、8桁で揃えようとしてもそろわず、ずれていることが分かる。

桁ぞろえを適切に行うためには、日本語は1文字2桁、英数字は1文字1桁でカウントする必要がある。文字コードがEUC-JPやSHIFT-JISであれば、文字を表現するために必要な情報量(日本語2バイト、英数字1バイト)を活用して、バイト数=桁数とすることで桁ぞろえが可能となる。しかし、大学ではUTF-8という日本語1文字あたり3バイトの文字コードを使用しているため、バイト数を桁数にしても桁ぞろえができない。これらを踏まえた上で次の方法で桁ぞろえを行う。

日本語を含む文字列の桁ぞろえの方法

require 'kconv'
   :
result = sprintf("%s %d %d %d %f\n",
         team.toeuc.force_encoding("binary"),data[0], data[1], 
         data[2], data[0]/(data[0]+data[1])).toutf8

puts result
require 'kconv'
文字コード変換を行うkconvライブラリを呼び出す。kconvを呼び出すことで、後述するtoeucやtoutf8メソッドが利用可能となる。
toeuc
文字コードをEUC-JPに変換する
toutf8
文字コードをUTF-8に変換する
force_encoding("binary")
文字コードをバイナリにする

これは、先ほどのプログラムの断片である。teamにチーム名が代入されている。このまま表示すると1文字1桁になってしまうので、1文字2桁になるように次の処理をしている。

  1. toeucメソッドで文字コードをEUC-JPに変更する。
  2. force_encoding("binary")でさらに文字コードをバイナリにする。
    • バイナリに変更すると日本語に変換されずに2進数のまま表現される
    • EUC-JPの場合1文字当たり16桁(2バイト)になる
    • バイナリモードに変換するとバイト数=桁になるので日本語1文字(2バイト)を2桁で表示できる
  3. 最後にtoutf8をつけて、表示する際の文字コードをUTF-8にする

このプログラムでは、そのまま結果を出力するのではなく、文字コードを変換した文字列を作成しているため、printfで出力されるものを文字列として返すsprintfメソッドを使って、UTF-8に変換した結果をresultに代入している。

[2]練習問題

result = sprintf("%s %d %d %d %f\n",

の行の%sや%dに幅指定の数字を追加し、上の実行結果と同じように結果をそろえて表示できるようにしてみよう。

[3]配列の生成と配列処理メソッド

配列処理メソッドとしてこれまで利用してきたのはlengthのみであったが、他にも多数のメソッドがある。ハッシュの並び替えを行うにあたり、今回は新たにreverseメソッドとsortメソッドを使用するが、その他のメソッドについても示しておく。

配列の逆順への並び替え(reverseメソッド)
配列の要素をすべて逆順に並べ替えた配列を返す
a = [50,20,80,10]の場合、
p a.reverse #=>[10,80,20,50]
p a #=>[50,20,80,10]

破壊的操作ではないのでaの値は変わらない。
配列の破壊的な逆順への並び替え(reverse!メソッド)
配列の要素をすべて破壊的に逆順に並べ替えた配列を返す。
a = [50,20,80,10]の場合、
p a.reverse! #=>[10,80,20,50]
p a #=>[10,80,20,50]

破壊的操作であるためaの値も変化する。
小さい順に並び替える(sortとsort!メソッド)
配列内の要素を小さい順(昇順)に並び替える。sortは破壊的メソッドではないが、sort!は破壊的メソッドとなる。
a = [50,20,80,10]の場合、
p a.sort #=>[10,20,50,80]
p a #=>[10,80,20,50]

sortは破壊的操作ではないのでaの値は変化しない。配列内の要素が文字の場合文字コードの小さい順に並び替えが行われる。

>>More

[4]配列における並び替え

ハッシュにおける結果の並び替えを考えるにあたり、まず配列の並び替え方法を確認しよう。

例えば、a=[192,168,0,255]という配列を仮定する。この配列内の4つの値を昇順(小さい順)に並び替えることを考えてみる。この場合、配列処理メソッドのsortを使用すればよい。sortは昇順(小さい順)に並び替えるメソッドである。

b = a.sort

とすることで、配列aの4つの値を小さい順に並び替えた配列bが完成する。すなわち配列bは、b=[0,168,192,255]となっている。

今度は配列内の値を降順(大きい順)に並び替えることを考えよう。この場合はsortメソッドに加えてreverseメソッドを使用する。reverseメソッドは配列内の値を逆順にするメソッドである。

b = a.reverse

とすると、配列aの中の値が逆順に並び替えられ、b=[255,0,168,192]となる。このままでは降順ではないが、sortメソッドと組み合わせればよい。まずsortメソッドで昇順に並び替えてから、reverseメソッドで逆順に並び替えると降順にした結果が得られる。すなわち下記のように書く。

b = a.sort.reverse

これにより、配列bb=[255,192,168,0]となる。配列内の値を昇順や降順に並べておけば、あとはインデックス0から順番に表示すればよい。

[5]ハッシュにおける並び替えの考え方

次に、ハッシュにおける並び替えを考えよう。それにあたり次のようなデータを考える。これらのデータを、名前をkey、得点をvalueとするハッシュ変数testに代入し、得点の低い順に並び替えることを考えてみよう。

名前   得点
一郎    72
二郎    48
三郎    96
四郎    33

ハッシュはkeyを指定してvalueを取り出す。このため、上の例では、四郎、二郎、一郎、三郎の順にkeyを指定して、keyとvalueを対で表示すれば
四郎    33
二郎    48
一郎    72
三郎    96
というように、得点の低い順に結果が並ぶ。

得点が低い順に結果を表示するには、valueの小さい順にkeyを並べておかなければならない。配列の場合は自分自身を並び替えるが、ハッシュの場合は、valueを基準としてkeyを並び替える。このように対象と基準が異なる並び替えを行う場合、sortメソッドにソート基準ブロックを付加する必要がある。

[6]sortメソッドへのソート基準ブロックの付加

sortメソッドには、ソート基準を決めるブロックを付加することができる。具体的には次のように書く。{ }内がソート基準ブロックに相当する。

配列.sort{|x,y| xyの比較式}

ソート基準ブロックを用いて昇順(小さい順)、降順(大きい順)に並び替えを行う場合、それぞれ以下のように書く。

  • 昇順:配列.sort{|a,b| a<=>b} (| |内と後ろのabの順が同じ)
  • 降順:配列.sort{|a,b| b<=>a} (| |内と後ろのabの順が異なる)

|a, b|は、配列内の値(要素)を2つ取り出してaとbに代入するという意味である。次のa<=>bは比較式と呼び、左辺と右辺を比較してb側が大きくなるように必要に応じてabの値を入れ替える。ここでb側とは|a, b|で2つ目に指定した変数側という意味である。

次に、配列から3つ目の値を取り出して<=>の右側の変数(例えばa<=>bならb)に代入し、abを比較してbに大きい値が来るようにする。 これを繰り返し、最終的に配列内の値を全て取り出した時点では次のようになっている。

  • 配列.sort{|a,b| a<=>b}の場合:aに最小値が入っている。bは配列から最後に取り出した値(最後に取り出した値が最小値の場合はそれまでaに入っていた値)が入っている。
  • 配列.sort{|a,b| b<=>a}の場合:bに最大値が入っている。aは配列から最後に取り出した値(最後に取り出した値が最大値の場合はそれまでbに入っていた値)が入っている。

<=>の左辺には配列内の最小値もしくは最大値が入っているので、1つ目の値として取り出す。残った値を用いて同じことをくり返すと2番目、3番目の値も確定する。

ちなみに、単にsortとすると昇順ソートになるが、これはソート基準ブロックに {|a, b| a<=>b} を指定したことと同義になる。sortやsort.reverseを使用する場合と比べ、ソート基準ブロックを使用する方法は複雑であるが、ハッシュのソートを行う際は必要不可欠となる。

なお、ソート基準ブロックの中でabという2つの変数を使っているが、これはxyでもjkでも何でも構わない。

ソート基準ブロックの基本形

  • 昇順:配列.sort{|a,b| a<=>b} (| |内と後ろのabの順が同じ)
  • 降順:配列.sort{|a,b| b<=>a} (| |内と後ろのabの順が異なる)

ここではabという2つの変数を使用しているが変数は任意の名称でよい。例えばxyでも良いし、hogepiyoでもよい。

[7]ハッシュの並び替え

ソート基準ブロックについて確認した上で、再度話を戻そう。ハッシュの並び替えは、valueを基準としたkeyの並び替えということになる。このため、まずハッシュからkeyを取り出す。ハッシュからkeyを取り出すためには

ハッシュ.keys

とする。keyではなくkeysであることに注意しよう。参考までにvalueのみを取り出す場合は

ハッシュ.values

となる。ここでもvalueではなくvaluesとなる。

ハッシュ変数testは、keyに名前、valueに得点が代入されているのでpメソッドでkeyを表示すると

p test.keys #=>["一郎","二郎","三郎","四郎"]

となる。次にkeyをvalueに基づいて昇順に並び替える。このためsortメソッドのソート基準ブロックではvalueを用いた比較式を指定する。

p test.keys.sort {|x,y| test[x] <=> test[y]}
#=>["四郎", "二郎", "一郎", "三郎"]

test[x]test[y]xyをkeyとするvalueに相当する。test.keys.sortでハッシュ変数testのkeyを取り出して並べ替えをしており、ソート基準ブロックの比較式ではtest[x]test[y]、すなわちvalueが使用されている。これによりvalueに基づいてkeyを並び替えることができる。なお、|x,y|とtest[x] <=> test[y]の部分でxyの順番が同じであるためkeyが昇順に並べ替えられる。

これをプログラムにしたものが、以下のsort.rbである。for-endでハッシュの値を取り出す際にソート基準ブロックを用いて、valueが小さい順に取り出している。

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

test  = {
  "一郎" => 72,
  "二郎" => 48,
  "三郎" => 96,
  "四郎" => 33,
}
for item in test.keys.sort{|a, b| test[a] <=> test[b]}
  printf("%s は %d点です。\n", item, test[item])
end

実行結果は以下の通りとなる。

sime{c1xxxxx}% chmod +x sort.rb[Return]
sime{c1xxxxx}% ./sort.rb[Return]
四郎は33点です。
二郎は48点です。
一郎は72点です。
三郎は96点です。

valueに基づいてkeyを並び替える

  • 昇順:hoge.keys.sort{|x,y| hoge[x]<=>hoge[y]} (| |内と後ろのxyの順が同じ)
  • 降順:hoge.keys.sort{|x,y| hoge[y]<=>hoge[x]} (| |内と後ろのxyの順が異なる)

ただしhogeはハッシュ変数とする。

[8]valueが配列の場合のハッシュのソート

本日の冒頭で取り上げたソフトボールの結果を、勝ち数の多い順に並べ変えてみよう。今度はsort.rbとは異なりvalueが配列になっている。配列内には勝ち数、負け数、引分数の3つの値が代入され、1つのkeyに結び付けられている。並び替えを行う場合には、sort.rbと同様にソート基準ブロックを用いるが、3つあるvalueのうちの勝ち数に基づいてソートをするということで若干書き方が異なってくる。

具体的には以下のように書く。

soft.keys.sort {|a, b| soft[b][0] <=> soft[a][0]}

ここで変数abにはkeyであるチーム名が入る。soft[a]soft[b]はkeyに対応するvalueである。valueは3つの値を持ち、第0要素の勝ち数を基準とするため、soft[a][0]soft[b][0]として勝ち数を指定している。なお|a, b|と後半のabの順番が逆であるため降順に並べ替えが行われる。

これを踏まえてプログラムを書き直すと以下の通りとなる(softball2.rb)。

#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-

require 'kconv'

soft = Hash.new

while line = gets
  if /(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/ =~ line
    # 1個目の( ) (\S+)→チーム名が入る
    # 2個目の( ) (\d+)→勝ち数が入る
    # 3個目の( ) (\d+)→負け数が入る
    # 4個目の( ) (\d+)→引分数が入る
    soft[$1] = [$2.to_f,$3.to_f,$4] # 配列を代入
  end
end

print"--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---\n"

for team in soft.keys.sort {|a, b| soft[b][0] <=> soft[a][0]}

  result =  sprintf("%-23s%d%6d%6d%9.3f\n",
              team.toeuc.force_encoding("binary"),
              soft[team][0], soft[team][1], soft[team][2],
              soft[team][0]/(soft[team][0]+soft[team][1])).toutf8
  puts result

end

このプログラムを実行すると以下のように、勝ち数の大きい順に結果が出力される。ただし、引分数の関係で、勝率の部分を見ると順位が逆転している箇所がある。

sime{c1xxxxx}% chmod +x softball2.rb[Return]
sime{c1xxxxx}% ./softball2.rb softball.txt[Return]
--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---
アンツ              78     53      5     0.595
ドラゴンフライズ         77     51      8     0.602
ブックウォームス         75     52      9     0.591
モスキートーズ          65     65      6     0.500
スネイルズ            52     82      2     0.388
クリケッツ            50     83      3     0.376

[9]レポート課題

以下のうちいずれかを選んで解答する(ruby2-3.rb)。

(1)softball2.rbの改良:できるところまで改良せよ

  1. 勝率の高い順に結果を表示できるようにする。結果は整えて表示すること。なお、softball.txt自体は書き換えないこと(7点)
  2. 勝ち数、負け数、引き分け数、勝率の昇順、降順いずれでも並び替えができるようにする(実行をしたら何にもとづいて並び替えをするか質問し、仮にユーザーが勝ちの多い順と指定したらその順番で表示できるようにする。getsがファイル読込とキーボード読込の2つの役割を担うことになる点に注意が必要。前期のopenメソッドの回を復習する必要あり)(8点)

(2)新商品の売上分析:公益フーズ株式会社は新商品A~Cを開発した。これらの商品が顧客の注意を引くように、広告を作成しようと考えているが、広告を作成するに当たり、コアとなる顧客層を特定する必要が生じた。これらの商品を購入する顧客に年代や性別などの偏りがある場合、それらを踏まえて広告を作成した方が訴求力が高いためである。

そこで、同社では傘下の公益マートでの1ヶ月間の先行販売を行い、売り上げ傾向を分析することにした。公益マートは県内に30店舗を展開しており、立地条件によって購入層に以下の傾向があることがわかっている。

  • 住宅街A:古くからある住宅街であり客層として多数を占めるのは日中の高齢者
  • 住宅街B:アパートの多い地域であり、夜間の(一人暮らしと思われる)若者の利用が多い
  • オフィス街:近隣の企業の昼間の会社員の利用が多い
  • 学校:校内にあり、在校生(学生)の利用が多い
  • 幹線道路:車での来客が多数を占め、客層や売上の多い時間帯に目立った特徴は無い

foods.txtは各店舗の1ヶ月間の販売個数のデータであり、店舗コード、商品A、商品B、商品Cの販売個数、立地条件が記載されている。このデータに基づき、以下のプログラムを出来るところまで作成しなさい(必ず1番から順に実施すること)。

  1. 商品A~Cのいずれかを選択すると、その商品の販売個数の多い順に、店舗コード、販売個数、立地の3つの情報が表示されるようにする(7点)
  2. 商品A~Cのいずれかを選択すると、30店舗全てでなくその商品の売上が多い上位10店舗について、1番と同じ情報が表示されるようにする(8点)
  3. 広告の効果について、経験的に以下の知見が得られているものとする
    • 特定の客層にターゲットを絞った広告の場合:その客層の販売個数1.5倍、その他の客層1.1倍
    • 客層を絞らない広告の場合:全ての客層の販売個数1.15倍
    特定の客層にターゲットを絞った広告を作成する上での条件を、「当該商品の売上が多い上位10店舗のうち5店舗に同一の立地条件を持つ店舗が含まれている」とした場合に、各商品についてどちらの広告戦略を適用すべきかを判定し、客層を絞る場合にはどの客層にすべきかを提案する。なお、客層とは「高齢者」「若者」「会社員」「学生」の4種類とする。(9点)
  4. 3番で提案された広告戦略を採用した場合の予想販売数を計算し、表示する。計算に当たっては以下を参考にすること(10点)
    • 広告のターゲットを高齢者に絞った場合:住宅街Aの販売数の合計×1.5+その他の店舗の販売数の合計×1.1
    • 広告のターゲットを若者に絞った場合:住宅街Bの販売数の合計×1.5+その他の店舗の販売数の合計×1.1
    • 広告のターゲットを会社員に絞った場合:オフィス街の販売数の合計×1.5+その他の店舗の販売数の合計×1.1
    • 広告のターゲットを学生に絞った場合:学校の販売数の合計×1.5+その他の店舗の販売数の合計×1.1
    • 広告のターゲットを絞らない場合:全店舗の販売数の合計×1.15
    • ターゲットが「幹線道路」になった場合:全店舗の販売数の合計×1.15

  • 提出先:課題提出用メールアドレス
  • 提出期限:第1提出期限、第2提出期限を設定
  • メールのSubject:課題3
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は下記の構成とする
  1. 何番の問題をどこまで実施したか
  2. 作成したプログラム
  3. プログラムの実行結果
  4. プログラムの説明
  5. 感想
  6. ファイルの添付

  • 採点基準:期限内提出点(2点)、メールの体裁(1点)、プログラム(2点~)、プログラムの説明(2点)
  • プログラムの説明:1番は変更点について説明すればよいが、単に「AをBに変更した」というように変更箇所を明記するだけでは不十分である。変更した理由が分かるように説明する。説明箇所は少ないが、その分しっかりと理由を書くことが求められる。2番はそれぞれの改良を施すにあたり重要であると考えるポイントについて述べる。プログラムに間違いがないことを示すために、foods.txtの内容を書き換えて、予想した結果になることを確認しておくとさらに良い。確認した場合はメールに具体的に記載すること。
  • わかりにくい説明や、Webページを単にコピー&ペーストしただけの説明は減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

Tips:emacsでの日本語入力のオンオフはCtrl+oです

Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする

Tips:Mewによるメールの送り方はMewコマンドを参照