制約充足問題として最強のボジョレーを求める

ちゃお......舞いおりん......†

今回は#ajiting Advent Calendar 2015の8日目ということでVOYAGE GROUPの多目的スペースAjitoにむりやり絡めて話そうと思います。

はじめに

先月の話になってしまいますが、今年もボジョレーヌーボーが解禁されました。

f:id:yukinoi:20151206034042j:plain

解禁日はAjitoでも終業後に飲んでる人がちらほらいたそうです (私はその日体調悪くて家に直帰しました...†)

さてボジョレーヌーボーといえば、毎年公開されるキャッチコピーが話題ですね。Wikipediaに年ごとのものが載っているのでちょっと挙げてみますとこんな感じです。

  • 1995年「ここ数年で一番出来が良い」
  • 1996年「10年に1度の逸品」
  • 1997年「まろやかで濃厚。近年まれにみるワインの出来で過去10年間でトップクラス」
  • 1998年「例年のようにおいしく、フレッシュな口当たり」
  • 1999年「1000年代最後の新酒ワインは近年にない出来」

どれが一番いいのかわかりません...😨

Related works

この問題に取り組んだ先行事例として、ボジョレーヌーボーの評価が面白すぎる!近年のキャッチコピーのまとめ! | 豆知識PRESS というWebページがあります。

それによると以下のようなランキングになってました。

1位  2009年
2位  2011年
3位  2010年

2010年は 1950年以降最高の出来と言われた2009年と同等の出来 と言われているのに、2010年と2009年の順位が違うのがなんだか納得いかない感じです。

うーん........................

ボジョレーソルバー

では論理的な整合性を担保しつつ最高のボジョレーを決めるにはどうすればいいでしょう?

毎年のキャッチコピーを不等式として表現すれば制約充足問題として解けるんじゃないか?

そう思いますよね?
DIY精神あふれるサイエンティストなら😎

ということで、ボジョレーソルバーを作ってみることにしました!

キャッチコピーを不等式にする

まずはWikipediaに載ってるキャッチコピーを不等式として表現します。

$ head catch_copies.txt
x1982 < x1985
x1983 < x1985
x1984 < x1985
x1991 < x1995
x1992 < x1995
x1993 < x1995
x1994 < x1995
x1986 < x1996
x1987 < x1996
x1988 < x1996

全部の制約は、GitHubに置いてます

なお、他と比較してない年はあんまりおいしくないんだろうなって思って除外してます。

また、ここでは曖昧性のある表現を以下のように解釈してます。

  • 近年 = 3年 (2年のときは「ここ2年で」と表現されてるので)
  • ここ数年 = 4年 (5年だったらキリがいいから5年って言いそう)
  • 近い出来 = 同じかそれ以下 (超えてたら「超す」って言いそう)
  • n年を思い起こさせます = n年と同じかそれ以下 (超えてたら「超す」って言いそう)

制約充足問題を解く

つづいて不等式の制約を充足する解を見つけるための部分を作ります。

ソルバーはMicrosoft ResearchのZ3を使います。Macリポジトリの最新版をコンパイルしようとしたらうまくいかなかったので、ReleasesのページにあるMac用バイナリを使いました。

from collections import defaultdict
import operator
import z3

OP = {
    '<': operator.lt,
    '<=': operator.le,
    '==': operator.eq,
    '!=': operator.ne,
    '>=': operator.ge,
    '>': operator.gt,
}


def parse(solver, catch_copies):
    for catch_copy in filter(bool, catch_copies.splitlines()):
        (lhs, op, rhs) = catch_copy.split()
        solver.add(OP[op](z3.Real(lhs), z3.Real(rhs)))
    return solver


def as_dict(solution):
    solution = solution.replace('[', '{')
    solution = solution.replace(']', '}')
    solution = solution.replace(' = ', '": ')
    solution = solution.replace('x', '"x')
    return eval(solution)


def ranking(d):
    indices = defaultdict(list)
    for (idx, val) in d.items():
        indices[val].append(idx)
    sorted_indices = sorted(indices.items(), reverse=True)
    for (rank, (_, indices)) in enumerate(sorted_indices, start=1):
        print(rank, ', '.join(sorted(indices)))


def solve(catch_copies):
    solver = parse(z3.Solver(), catch_copies)
    print('solution:')
    print(solver.check())
    print(solver.model())
    print('ranking:')
    d = as_dict(solver.model().__str__())
    ranking(d)


if __name__ == '__main__':
    import sys
    catch_copy_file = sys.argv[1]
    catch_copies = open(catch_copy_file).read()
    solve(catch_copies)

それぞれの年のワインに数値を割り当てて、不等式の制約を満たす組み合わせを求めます。 そして、一番おいしい年に最大値が割り当てられます。

いちおう来年もネタを使いまわせるようにGitHubにも置いておきました

結果

さっそく実行!

$ python beaujolais_solver.py catch_copies.txt
solution:
sat
[x1915 = -2,
 x1922 = -2,
 x1936 = -2,
 x1943 = -2,
 x2003 = -1,
 x1972 = -2,
 x1994 = -6,
 x2010 = 0,
 x1993 = -6,
 x2008 = -1,
 x1906 = -2,
 x1996 = -4,
 x1963 = -2,
 x1969 = -2,
 x1898 = -2,
 x1965 = -2,
 x1975 = -2,
 x1983 = -3,
 x2015 = 0,
 x1947 = -2,
 x2000 = -4,
 x1985 = -2,
 x1970 = -2,
 x1934 = -2,
 x1950 = -2,
 x1924 = -2,
 x2006 = -2,
 x1966 = -2,
 x1981 = -2,
 x1893 = -2,
 x1920 = -2,
 x1930 = -2,
 x1960 = -2,
 x1935 = -2,
 x1903 = -2,
 x1964 = -2,
 x1916 = -2,
 x1899 = -2,
 x1921 = -2,
 x1927 = -2,
 x1931 = -2,
 x1952 = -2,
 x1962 = -2,
 x1976 = -2,
 x1941 = -2,
 x1896 = -2,
 x1974 = -2,
 x2011 = 1,
 x1968 = -2,
 x1928 = -2,
 x1925 = -2,
 x1911 = -2,
 x1929 = -2,
 x1940 = -2,
 x1894 = -2,
 x2005 = 0,
 x1958 = -2,
 x1913 = -2,
 x1926 = -2,
 x1999 = -4,
 x1909 = -2,
 x1959 = -2,
 x1997 = -5,
 x1982 = -3,
 x1938 = -2,
 x1948 = -2,
 x1949 = -2,
 x1942 = -2,
 x1986 = -5,
 x1946 = -2,
 x1971 = -2,
 x1957 = -2,
 x2009 = 0,
 x2007 = -1,
 x1979 = -2,
 x1918 = -2,
 x1967 = -2,
 x2001 = -3,
 x1990 = -5,
 x1907 = -2,
 x1917 = -2,
 x1919 = -2,
 x1901 = -2,
 x1951 = -2,
 x1933 = -2,
 x1978 = -2,
 x1977 = -2,
 x1988 = -5,
 x1902 = -2,
 x1991 = -6,
 x1987 = -5,
 x1980 = -2,
 x1897 = -2,
 x1992 = -6,
 x1932 = -2,
 x1953 = -2,
 x1908 = -2,
 x1955 = -2,
 x1954 = -2,
 x1984 = -3,
 x1939 = -2,
 x1912 = -2,
 x1904 = -2,
 x1956 = -2,
 x1961 = -2,
 x1905 = -2,
 x2002 = -2,
 x1989 = -5,
 x1995 = -5,
 x1923 = -2,
 x1937 = -2,
 x1973 = -2,
 x1895 = -2,
 x1944 = -2,
 x1945 = -2,
 x1998 = -5,
 x1900 = -2,
 x1910 = -2,
 x2004 = -1,
 x1914 = -2]
ranking:
1 x2011
2 x2005, x2009, x2010, x2015
3 x2003, x2004, x2007, x2008
4 x1893, x1894, x1895, x1896, x1897, x1898, x1899, x1900, x1901, x1902, x1903, x1904, x1905, x1906, x1907, x1908, x1909, x1910, x1911, x1912, x1913, x1914, x1915, x1916, x1917, x1918, x1919, x1920, x1921, x1922, x1923, x1924, x1925, x1926, x1927, x1928, x1929, x1930, x1931, x1932, x1933, x1934, x1935, x1936, x1937, x1938, x1939, x1940, x1941, x1942, x1943, x1944, x1945, x1946, x1947, x1948, x1949, x1950, x1951, x1952, x1953, x1954, x1955, x1956, x1957, x1958, x1959, x1960, x1961, x1962, x1963, x1964, x1965, x1966, x1967, x1968, x1969, x1970, x1971, x1972, x1973, x1974, x1975, x1976, x1977, x1978, x1979, x1980, x1981, x1985, x2002, x2006
5 x1982, x1983, x1984, x2001
6 x1996, x1999, x2000
7 x1986, x1987, x1988, x1989, x1990, x1995, x1997, x1998
8 x1991, x1992, x1993, x1994

2011年が一番おいしいボジョレーだそうです (キャッチコピーが全て正しいなら)。でも、we regret to inform you that 2011年のボジョレーはもう飲めません🙅

一般的なワインは熟成中の状態で出荷され、温度管理されたワインセラーに置くことで味が良くなっていくが、ボジョレーヌヴォーはそれ以上熟成しない状態で出荷されるため、値段と比べれば味はそこそこだが長期保存できない(あとは劣化するのみ)という特徴がある。そのため、ボジョレーヌヴォーは年内に消費することがおすすめされている。

ということなんです。WIkipediaより

ってことは「110年ぶりの当たり年」って謳ってる2003年のコピーを書いた人は一体...😨

おわりに

本記事では、毎年公開されるキャッチコピーを不等式に変換し制約充足問題として定式化することによって毎年変わるボジョレーヌヴォーの美味しさの順を求める方法を提案しました。これによって、キャッチコピーから美味しさの相対的な位置づけがわかるようになったので、来年以降は飲まなくてもどの程度の美味しさかなんとなくわかるようになりました (多分)。

ちなみにワイン通の人の感覚とどれくらい合致しているかは、気兼ねなく聞けるワイン通が周りにいないのでわかりません笑

またとても大事なことですが、キャッチコピーを書いた人が全部同じ人でないとそもそもそれぞれの不等式を同じようにつなげていいのかという問題もあります。

というわけで過去にグレートヴィンテージと言われた2009年を思い起こさずにはいられないお話でした。

追記

という反応をいただいてから、全部の制約を記述できてなかったことに気付きました。。。 最初はpython-constraintっていう遅いソルバーで試して解けなかったから変数削ってそのままにしてたんです。最終的に採用したZ3ソルバーは速いので削った制約を全部入れても解けました!というわけで↑の結果は全部の制約入れた版のものに挿しかえてます。(変更前は2002年と2011年が1位だった)