水道屋がRubyを活用する(その4)

ずいぶん久しぶりですが、私が仕事で使っているRubyプログラムの一部を紹介します。
その4となってますが、前のはここにあります。

http://d.hatena.ne.jp/miura1729/20090428/1240924372 その1
http://d.hatena.ne.jp/miura1729/20090430/1241083520 その2
http://d.hatena.ne.jp/miura1729/20090502/1241259378 その3

今回はその3のところで、ExcelのデータをRuby+Win32OLEを使って単価表を作ったわけですが、実際の見積でこれを使う話です。

水道屋が扱う部品はとても多くてサイズの違いなども別々に数えると数千にも及びます。それだけ種類が多くなると、それぞれを区別するために部品の名前が長くなってしまいます。さらに、使用頻度の高いものは略称をつい使ってしまいます。そんなこんなで、単価表に載っている部品は表記が揺れまくっています。
そのため、見積をしようとすると、通常の検索ではまともに部品が拾えないことになります。そこで、表記に揺れがあってもある程度適当に検索するfuzzy_compareというメソッドを作りました。これを使うと結構それらしい部品を探して提示してくれます。ただし、可能性が高いというだけで最終的な判断は人間がやる必要があります。それでも、数千もの候補から探すの事を考えると何10倍もスピードアップします。

fuzzy_compareはN-gramアルゴリズムを基本に、水道の部品の特有の特徴(φ[数字]+は口径で部品を特定するのに非常に重要であるとか)、を加味しています。かなり適当なつくりですが、結構重宝しています。

ソースコード兼使用例です。

# -*- coding: cp932 -*-
def fuzzy_compare(s1, s2)
  score = 0
  s1a = s1.each_char.to_a
  we = 50
  s1a.each_cons(2) do |cp1|
    s = cp1.join
    if s2.include?(s) then
      score = score + we
      we = (we / 2 + 10)
    end
  end

  we = 40
  s1a.each_cons(3) do |cp1|
    s = cp1.join
    if s2.include?(s) then
      score = score + we
      we = (we / 2 + we / 3 + 10)
    end
  end
  we = 100
  s1.scan(/[0-9]+/).each do |cn1|
    if /([^0-9]#{cn1}[^0-9])|([^0-9]#{cn1}$)/ =~ s2 then
      we = (cn1.sub(/[0]+$/, "").size) * 100
      score = score + we
    end
  end
  score
end

def search(list, s)
  l = []
  list.each do |e|
    l.push [fuzzy_compare(s, e), e]
  end

  l.sort_by {|e0| -e0[0]}
end

list = [
 "硬質塩化ビニル管 φ100 VP",
 "硬質塩化ビニル管 φ75 VP",
 "硬質塩化ビニル管 φ75 HI",
 "メカ式ベンド φ100",
 "メカ式ベンド φ50",
 "ダグタイル鋳鉄管 φ100 NS",
 "ダグタイル鋳鉄管 φ75 NS"
]

["塩ビ管 100", "ベンド 100", "鋳鉄管 100"].each do |s|
  print "#{s} \n"
  search(list, s).each do |e|
    print "  #{e[1]} -> #{e[0]}\n"
  end
  print "\n"
end

これで、

硬質塩化ビニル管 φ100 VP
硬質塩化ビニル管 φ75 VP
硬質塩化ビニル管 φ75 HI
メカ式ベンド φ100
メカ式ベンド φ50
ダグタイル鋳鉄管 φ100 NS
ダグタイル鋳鉄管 φ75 NS

という部品表から

塩ビ管 100
ベンド 100
鋳鉄管 100

を検索した場合の結果です。数が大きいほどより一致していると判断します。通常、数が大きい順にソートして表示します。

ruby fuzzy.rb
塩ビ管 100 
  硬質塩化ビニル管 φ100 VP -> 359
  ダグタイル鋳鉄管 φ100 NS -> 318
  メカ式ベンド φ100 -> 225
  硬質塩化ビニル管 φ75 HI -> 175
  硬質塩化ビニル管 φ75 VP -> 175
  ダグタイル鋳鉄管 φ75 NS -> 125
  メカ式ベンド φ50 -> 0

ベンド 100 
  メカ式ベンド φ100 -> 589
  メカ式ベンド φ50 -> 399
  硬質塩化ビニル管 φ100 VP -> 225
  ダグタイル鋳鉄管 φ100 NS -> 225
  硬質塩化ビニル管 φ75 HI -> 0
  硬質塩化ビニル管 φ75 VP -> 0
  ダグタイル鋳鉄管 φ75 NS -> 0

鋳鉄管 100 
  ダグタイル鋳鉄管 φ100 NS -> 589
  ダグタイル鋳鉄管 φ75 NS -> 399
  硬質塩化ビニル管 φ100 VP -> 318
  メカ式ベンド φ100 -> 225
  硬質塩化ビニル管 φ75 VP -> 125
  硬質塩化ビニル管 φ75 HI -> 125
  メカ式ベンド φ50 -> 0