多键多值非确定性Python字典

11

Python已经有多键字典和多值字典。我需要一个既能实现多键又能实现多值的Python字典。

例子:

# probabilistically fetch any one of baloon, toy or car
d['red','blue','green']== "baloon" or "car" or "toy"  

Probability of d['red'] 等于 d['green'] 的概率很高,而 Probability of d['red'] 不等于 d['red'] 的概率很低但可能发生。
单个输出值应基于键的规则进行模糊的概率确定。例如,在上述情况下,规则可以是如果键同时具有“红色”和“蓝色”,则80%的时间返回“气球”,如果仅为蓝色,则15%的时间返回“玩具”,否则5%的时间返回“汽车”。
setitem方法应设计为可以实现以下操作:
d["red", "blue"] =[
    ("baloon",haseither('red','green'),0.8),
    ("toy",.....)
    ,....
]

上面使用谓词函数和相应的概率将多个值分配给字典。与上面的分配列表不同,甚至将字典作为分配更可取:
d["red", "blue"] ={ 
    "baloon": haseither('red','green',0.8),
    "toy": hasonly("blue",0.15),
    "car": default(0.05)
}

在上述气球中,如果存在“红色”或“绿色”,则将80%的时间返回;如果存在“蓝色”,则15%的时间返回玩具;如果没有任何条件,则5%的时间返回汽车。
是否有现有的数据结构在Python中已满足上述要求?如果没有,则如何修改multikeydict代码以满足上述要求?
如果使用字典,则可以有一个配置文件或适当的嵌套装饰器,该文件或装饰器可配置上述概率谓词逻辑,而无需硬编码if \ else语句。
注意:以上是基于规则的自动回复应用程序的有用自动机,因此,请告诉我是否有任何类似的基于规则的框架可用于Python,即使它不使用字典结构?

d['red']==d['green'] 是什么意思?这是因为您之前查找过包含这两个键的多键吗?我理解您的问题是指 d['red'] != d['red'] 不一定成立... - Andy Hayden
@AndyHayden 是的,百分比由用户提供。 - stackit
@stackit 目前/在你的例子中,百分比不相加。它是不完整的 - 有更多的情况需要处理。 - Andy Hayden
1
WoJ 的想法是正确的,这个东西不需要像字典一样 - 它是一个函数。让它像字典一样会使它变得晦涩难懂。:( - Andy Hayden
1
设置一个项目怎么样?假设你有 d['red','blue','green']= "baloon" or "car" or "toy",并为这些物品设置了概率。如果执行 d['red']='ball' 会发生什么? - dawg
显示剩余13条评论
4个回答

4

模拟多关键字字典

multi_key_dict不支持同时使用多个关键字进行__getitem__()操作...

(例如:d["red", "green"])

一个多键可以使用 tupleset 键模拟。如果顺序不重要,set 似乎是最好的选择(实际上是可哈希的 frozen set,所以 ["red", "blue"]["blue", "red"] 是相同的)。

模拟多值字典

Multi values 是使用某些数据类型固有的,它可以是一个方便索引的 任何存储元素。标准的 dict 应该提供这个功能。

非确定性

使用由规则和假设定义的概率分布1,使用 python文档中的此处配方 进行非确定性选择。

MultiKeyMultiValNonDeterministicDict

多么奇怪的名字。  \o/-太棒了!

该类接受多个键,定义了一个由多个值组成的概率规则集。在创建项(__setitem__())时,所有键的值的概率都被预先计算出来,适用于所有键组合1。在访问项(__getitem__())时,会选择预先计算好的概率分布,并根据随机加权选择进行评估。

定义

import random
import operator
import bisect
import itertools

# or use itertools.accumulate in python 3
def accumulate(iterable, func=operator.add):
    'Return running totals'
    # accumulate([1,2,3,4,5]) --> 1 3 6 10 15
    # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
    it = iter(iterable)
    try:
        total = next(it)
    except StopIteration:
        return
    yield total
    for element in it:
        total = func(total, element)
        yield total

class MultiKeyMultiValNonDeterministicDict(dict):

    def key_combinations(self, keys):
        """get all combinations of keys"""
        return [frozenset(subset) for L in range(0, len(keys)+1) for subset in itertools.combinations(keys, L)]

    def multi_val_rule_prob(self, rules, rule):
        """
        assign probabilities for each value, 
        spreading undefined result probabilities
        uniformly over the leftover results not defined by rule.
        """
        all_results = set([result for result_probs in rules.values() for result in result_probs])
        prob = rules[rule]
        leftover_prob = 1.0 - sum([x for x in prob.values()])
        leftover_results = len(all_results) - len(prob)
        for result in all_results:
            if result not in prob:
                # spread undefined prob uniformly over leftover results
                prob[result] = leftover_prob/leftover_results
        return prob

    def multi_key_rule_prob(self, key, val):
        """
        assign probability distributions for every combination of keys,
        using the default for combinations not defined in rule set
        """ 
        combo_probs = {}
        for combo in self.key_combinations(key):
            if combo in val:
                result_probs = self.multi_val_rule_prob(val, combo).items()
            else:
                result_probs = self.multi_val_rule_prob(val, frozenset([])).items()
            combo_probs[combo] = result_probs
        return combo_probs

    def weighted_random_choice(self, weighted_choices):
        """make choice from weighted distribution"""
        choices, weights = zip(*weighted_choices)
        cumdist = list(accumulate(weights))
        return choices[bisect.bisect(cumdist, random.random() * cumdist[-1])]

    def __setitem__(self, key, val):
        """
        set item in dictionary, 
        assigns values to keys with precomputed probability distributions
        """

        precompute_val_probs = self.multi_key_rule_prob(key, val)        
        # use to show ALL precomputed probabilities for key's rule set
        # print precompute_val_probs        

        dict.__setitem__(self, frozenset(key), precompute_val_probs)

    def __getitem__(self, key):
        """
        get item from dictionary, 
        randomly select value based on rule probability
        """
        key = frozenset([key]) if isinstance(key, str) else frozenset(key)             
        val = None
        weighted_val = None        
        if key in self.keys():
            val = dict.__getitem__(self, key)
            weighted_val = val[key]
        else:
            for k in self.keys():
                if key.issubset(k):
                    val = dict.__getitem__(self, k)
                    weighted_val = val[key]

        # used to show probabality for key
        # print weighted_val

        if weighted_val:
            prob_results = self.weighted_random_choice(weighted_val)
        else:
            prob_results = None
        return prob_results

使用方法

d = MultiKeyMultiValNonDeterministicDict()

d["red","blue","green"] = {
    # {rule_set} : {result: probability}
    frozenset(["red", "green"]): {"ballon": 0.8},
    frozenset(["blue"]): {"toy": 0.15},
    frozenset([]): {"car": 0.05}
}

测试

检查概率

N = 10000
red_green_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
red_blue_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
blue_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
red_blue_green_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
default_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}

for _ in xrange(N):
    red_green_test[d["red","green"]] += 1.0
    red_blue_test[d["red","blue"]] += 1.0
    blue_test[d["blue"]] += 1.0
    default_test[d["green"]] += 1.0
    red_blue_green_test[d["red","blue","green"]] += 1.0

print 'red,green test      =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_green_test.items())
print 'red,blue test       =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_blue_test.items())
print 'blue test           =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in blue_test.items())
print 'default test        =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in default_test.items())
print 'red,blue,green test =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_blue_green_test.items())

red,green test      = car: 09.89% toy: 10.06% ballon: 80.05%
red,blue test       = car: 05.30% toy: 47.71% ballon: 46.99%
blue test           = car: 41.69% toy: 15.02% ballon: 43.29%
default test        = car: 05.03% toy: 47.16% ballon: 47.81%
red,blue,green test = car: 04.85% toy: 49.20% ballon: 45.95%

概率符合规则!


注脚

  1. Distribution Assumption

    Since the rule set is not fully defined, assumptions are made about the probability distributions, most of this is done in multi_val_rule_prob(). Basically any undefined probability will be spread uniformly over the remaining values. This is done for all combinations of keys, and creates a generalized key interface for the random weighted selection.

    Given the example rule set

    d["red","blue","green"] = {
        # {rule_set} : {result: probability}
        frozenset(["red", "green"]): {"ballon": 0.8},
        frozenset(["blue"]): {"toy": 0.15},
        frozenset([]): {"car": 0.05}
    }
    

    this will create the following distributions

    'red'           = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'green'         = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'blue'          = [('car', 0.425), ('toy', 0.150), ('ballon', 0.425)]
    'blue,red'      = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'green,red'     = [('car', 0.098), ('toy', 0.098), ('ballon', 0.800)]
    'blue,green'    = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'blue,green,red'= [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
     default        = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    

    If this is incorrect, please advise.


看起来这是一个很棒的实现,谢谢,我很快会检查它并分享任何疑问。我对此有很多赞美之词,但评论系统不允许。 - stackit
在默认情况下,您使用了“green”键,这是什么让您认为“green”是默认键? - stackit
此外,字典的键应该是frozenset类型,因为键的顺序并不重要,而且需要是可哈希的。我之前只是用这个结构来举例说明。 - stackit
你没说它必须高效,哈哈! :) 如果有那么多的键,那么预计算部分应该实时评估。 - tmthydvnprt
这需要什么类型检查? - stackit
显示剩余7条评论

4
单一输出值应基于从键中的规则进行模糊概率确定。例如,在上述情况下,规则可以是:如果键同时有“红色”和“蓝色”,则80%的概率返回“气球”,如果只有“蓝色”,则15%的概率返回“玩具”,否则以5%的概率返回“汽车”。请注意,你的案例分析不完整且含糊不清,但你可以在“精神上”完成以下工作(详细说明所需结果)。
import random

def randomly_return(*colors):
    colors = set(*colors)
    if 'red' in colors and 'blue' in colors:
        if random.random() < 0.8:  # 80 % of the time
            return "baloon"

    if 'blue' in colors and len(colors) == 1:  # only blue in colors
        if random.random() < 0.15:
            return "toy"
        else:
            if random.random() < 0.05:
                return "car"

# other cases to consider

我会将其保留为函数,因为它本来就是一个函数!但如果您坚持要将其变成类似字典的形式,那么可以通过覆盖 __getitem__ 来实现(但在我看来这并不符合 Pythonic 风格)。
class RandomlyReturn(object):
    def __getitem__(self, *colors):
        return randomly_return(*colors)

>>> r = RandomlyReturn()
>>> r["red", "blue"]  # 80% of the time it'll return "baloon"
"baloon"

根据您的说明,OP希望传递和生成:

randreturn((haseither(red,blue),baloon:0.8),((hasonly(blue),toy:0.15)),(default(‌​),car:0.05)))

您想要生成以下函数:

funcs = {"haseither": lambda needles, haystack: any(n in haystack for n in needles),
         "hasonly": lambda needles, haystack: len(needles) == 1 and needles[1] in haystack}

def make_random_return(crits, default):
    def random_return(*colors):
        colors = set(*colors)
        for c in crits:
            if funcs[c["func"]](c["args"], colors) and random.random() > c["with_prob"]:
                return c["return_value"]
        return default
    return random_return

在这种情况下,crit 和 default 将是什么:
crit = [{"func": "haseither", "args": ("red", "blue"), "return_value": "baloon", "with_prob": 0.8}, ...]
default = "car"  # ??
my_random_return = make_random_return(crits, default)

如我所说,你的概率不明确/不合理,因此你很可能需要调整这个...。
你可以通过传递crit和default来扩展类定义:
class RandomlyReturn(object):
    def __init__(self, crit, default):
        self.randomly_return = make_random_return(crit, default)
    def __getitem__(self, *colors):
        return self.randomly_return(*colors)

>>> r = RandomlyReturn(crit, default)
>>> r["red", "blue"]  # 80% of the time it'll return "baloon"
"baloon"

getitem将使用上述的装饰器。 - stackit
randreturn((has(red,blue),baloon:0.8),(...)) - stackit
上面是一个嵌套的装饰器,用于字典定义中的getitem函数。 - stackit
@stackit 这不是一个装饰器,因为装饰器接受一个函数并返回一个新函数。你需要一个函数,它接受条件并返回一个函数(然后你可以生成一个类或其他内容)。 - Andy Hayden
1
@NizamMohamed,你可以随时修改上面的代码或编写一个新的答案(最好是新的)。 - stackit
显示剩余20条评论

1
原文为英语,翻译如下:

OP想要的是:

d["red", "blue"] ={ 
    "baloon": haseither('red','green',0.8),
    "toy": hasonly("blue",0.15),
    "car": default(0.05)
}  

但这是带有嵌入逻辑的数据。为每个值定义一个函数非常繁琐。我的建议是将数据和逻辑分离。
Python 有一个数据类型可以实现这一点,那就是“class”。可以将可调用的“class”实例分配给“dict”,并让“dict”传递键并调用对象返回结果。
我继承并扩展了“multiple_key_dict”以支持多键获取,并将键传递给存储在字典中的对象并调用该对象。
我假设数据根据规则重新计算。这是一个名为“Rule”的类,它具有规则列表。规则是 Python 表达式,可以访问“len”函数和“keys”列表。因此,可以编写像“len(keys) == 1 and 'blue' in keys”的规则。
class Rule(object):

    def __init__(self, rule, data):
        self.rule = rule
        self.data = data

这是一个名为 Data 的类,它包含了数据和规则两部分。
class Data(object):
    def __init__(self, rules):
        self.rules= rules

    def make_choice(self, data):
        data = tuple(self.make_list_of_values(data))
        return random.choice(data)

    def make_list_of_values(self, data):
        for val, weight in data:
            percent = int(weight * 100)
            for v in [val] * percent:
                yield v

    def __call__(self, keys):
        for rule in self.rules:
            if eval(rule.rule,dict(keys=keys)):
                return self.make_choice(rule.data)

这是RuleDict,但不可调用的内容无法获取。
class RuleDict(multi_key_dict):
    def __init__(self, *args, **kwargs):
        multi_key_dict.__init__(self, *args, **kwargs)

    def __getitem__(self, keys):
        if isinstance(keys, str):
            keys = (keys, )
        keys_set = frozenset(keys)
        for key in self.keys():
            key = frozenset(key)
            if keys_set <= key:
                return multi_key_dict.__getitem__(self,keys[0])(keys)
        raise KeyError(keys)

使用示例。
d = RuleDict()
rule1 = Rule('"red" in keys and "green" in keys',(('baloon',0.8), ('car',0.05), ('toy',0.15)))
rule2 = Rule('len(keys) ==1 and "blue" in keys',(('baloon',0.25), ('car',0.35), ('toy',0.15)))
data = Data((rule1, rule2))
d['red','blue','green'] = data

print(d['red','green'])  

d['red','green'] 调用被赋值的具有键的对象并返回结果。

另一种方法是使 dict 可调用。这似乎是一个合理的方法,因为数据和逻辑是分开的。通过这种方式,您将键和可调用的逻辑传递给字典并返回结果。例如,

def f(keys, data):
    pass # do the logic and return data

d['red','blue','green'] = ('baloon', 'car', 'toy')

现在调用dict
d(('red','blue'),f)

这是可调用的dict。如果没有给定可调用对象,将返回整个数据。
class callable_mkd(multi_key_dict):
    def __init__(self, *args, **kwargs):
        multi_key_dict.__init__(self, *args, **kwargs)

    def __call__(self, keys, process=None):
        keys_set = frozenset(keys)
        for key in self.keys():
            key = frozenset(key)
            if keys_set <= key:
                if process:
                    return process(keys, self[keys[0]])
                return self[keys[0]]
        raise KeyError(keys)

1
如果可以改变数据结构,那么编写一个返回所需数据的函数会更简单。这将完全灵活,并且可以适应任何类型的数据,如果您以后需要更改它们。
import random

def myfunc(*args):
    if 'red' in args:
        return 'blue'
    elif 'green' in args or 'violet' in args:
        return 'violet'
    else:
        r = random.random()
        if 0 < r < 0.2:
            return 'blue'
        else:
            return 'green'

print(myfunc('green', 'blue'))
print(myfunc('yellow'))

输出(第二行显然会发生变化):

violet
blue

是的,这是可能的,你可以举一个例子。 - stackit
谢谢,但是如何将其纳入多字典结构中? - stackit
1
类也可以有这个功能,您会如何将其合并到多键字典中? - stackit
我不确定我理解了什么:类中的函数(方法)是“普通”的函数,只是在特定的上下文中。再说一遍:字典保存确定性值,因此您的“模糊”答案不能直接实现(您可以预先计算值(一次或按时计算),以便在字典中存储的任何内容仍然具有确定性,但具有随时间变化的值)。但我很难理解为什么您要使用字典而不是函数。 - WoJ
希望使用字典,因为它很方便、易于使用,并且使代码更易于维护。 - stackit
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接