[74] | 1 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
| 2 | from geniusweb.opponentmodel.OpponentModel import OpponentModel
|
---|
| 3 | from decimal import Decimal
|
---|
| 4 | from geniusweb.issuevalue.Domain import Domain
|
---|
| 5 | from geniusweb.issuevalue.Bid import Bid
|
---|
| 6 | from typing import Dict, Optional
|
---|
| 7 | from geniusweb.issuevalue.Value import Value
|
---|
| 8 | from geniusweb.actions.Action import Action
|
---|
| 9 | from geniusweb.progress.Progress import Progress
|
---|
| 10 | from geniusweb.actions.Offer import Offer
|
---|
| 11 | from geniusweb.references.Parameters import Parameters
|
---|
| 12 | from geniusweb.utils import val, HASH, toStr
|
---|
| 13 |
|
---|
| 14 |
|
---|
| 15 | class FrequencyOpponentModel(UtilitySpace, OpponentModel):
|
---|
| 16 | '''
|
---|
| 17 | implements an {@link OpponentModel} by counting frequencies of bids placed by
|
---|
| 18 | the opponent.
|
---|
| 19 | <p>
|
---|
| 20 | NOTE: {@link NumberValue}s are also treated as 'discrete', so the frequency
|
---|
| 21 | of one value does not influence the influence the frequency of nearby values
|
---|
| 22 | (as you might expect as {@link NumberValueSetUtilities} is only affected by
|
---|
| 23 | the endpoints).
|
---|
| 24 | <p>
|
---|
| 25 | immutable.
|
---|
| 26 | '''
|
---|
| 27 |
|
---|
| 28 | _DECIMALS = 4 # accuracy of our computations.
|
---|
| 29 |
|
---|
| 30 | def __init__(self, domain: Optional[Domain],
|
---|
| 31 | freqs: Dict[str, Dict[Value, int]], total: int,
|
---|
| 32 | resBid: Optional[Bid]):
|
---|
| 33 | '''
|
---|
| 34 | internal constructor. DO NOT USE, see create. Assumes the freqs keyset is
|
---|
| 35 | equal to the available issues.
|
---|
| 36 |
|
---|
| 37 | @param domain the domain. Should not be None
|
---|
| 38 | @param freqs the observed frequencies for all issue values. This map is
|
---|
| 39 | assumed to be a fresh private-access only copy.
|
---|
| 40 | @param total the total number of bids contained in the freqs map. This
|
---|
| 41 | must be equal to the sum of the Integer values in the
|
---|
| 42 | {@link #bidFrequencies} for each issue (this is not
|
---|
| 43 | checked).
|
---|
| 44 | @param resBid the reservation bid. Can be null
|
---|
| 45 | '''
|
---|
| 46 | self._domain = domain
|
---|
| 47 | self._bidFrequencies = freqs
|
---|
| 48 | self._totalBids = total
|
---|
| 49 | self._resBid = resBid
|
---|
| 50 |
|
---|
| 51 | """
|
---|
| 52 | These variables are dictionaries with all issues of the domain as their keys. '_BidsChangedFrequency' and
|
---|
| 53 | '_previousIssueValue' are helper-structures to construct the final structure: '_issueWeights', which holds the
|
---|
| 54 | estimated weight of any issue.
|
---|
| 55 | """
|
---|
| 56 | self._BidsChangedFrequency = {
|
---|
| 57 | key: 0 for key in self._bidFrequencies.keys()}
|
---|
| 58 | self._previousIssueValue = {
|
---|
| 59 | key: None for key in self._bidFrequencies.keys()}
|
---|
| 60 | self._issueWeights = {key: Decimal(
|
---|
| 61 | 1/len(self._bidFrequencies)) for key in self._bidFrequencies.keys()}
|
---|
| 62 |
|
---|
| 63 | @staticmethod
|
---|
| 64 | def create() -> "FrequencyOpponentModel":
|
---|
| 65 | return FrequencyOpponentModel(None, {}, 0, None)
|
---|
| 66 |
|
---|
| 67 | # Override
|
---|
| 68 | def With(self, newDomain: Domain, newResBid: Optional[Bid]) -> "FrequencyOpponentModel":
|
---|
| 69 | if newDomain == None:
|
---|
| 70 | raise ValueError("domain is not initialized")
|
---|
| 71 | # FIXME merge already available frequencies?
|
---|
| 72 | return FrequencyOpponentModel(newDomain,
|
---|
| 73 | {iss: {}
|
---|
| 74 | for iss in newDomain.getIssues()},
|
---|
| 75 | 0, newResBid)
|
---|
| 76 |
|
---|
| 77 | """
|
---|
| 78 | The original implementation provided by Geniusweb calculates the utility for a bid with equal weights for each issue:
|
---|
| 79 | '1 / (all issues present in the domain)' Instead of that we now use individual weights for each issue.
|
---|
| 80 | """
|
---|
| 81 | # Override
|
---|
| 82 |
|
---|
| 83 | def getUtility(self, bid: Bid) -> Decimal:
|
---|
| 84 | if self._domain == None:
|
---|
| 85 | raise ValueError("domain is not initialized")
|
---|
| 86 | if self._totalBids == 0:
|
---|
| 87 | return Decimal(1)
|
---|
| 88 | sum = Decimal(0)
|
---|
| 89 |
|
---|
| 90 | for issue in val(self._domain).getIssues():
|
---|
| 91 | if issue in bid.getIssues():
|
---|
| 92 | sum += (self._issueWeights[issue] *
|
---|
| 93 | self._getFraction(issue, val(bid.getValue(issue))))
|
---|
| 94 | return round(sum, FrequencyOpponentModel._DECIMALS)
|
---|
| 95 |
|
---|
| 96 | # Override
|
---|
| 97 | def getName(self) -> str:
|
---|
| 98 | if self._domain == None:
|
---|
| 99 | raise ValueError("domain is not initialized")
|
---|
| 100 | return "FreqOppModel" + str(hash(self)) + "For" + str(self._domain)
|
---|
| 101 |
|
---|
| 102 | # Override
|
---|
| 103 | def getDomain(self) -> Domain:
|
---|
| 104 | return val(self._domain)
|
---|
| 105 |
|
---|
| 106 | """
|
---|
| 107 | Since this method updates the model with every offer, this is also where we update our
|
---|
| 108 | weights-estimation-variables.
|
---|
| 109 | """
|
---|
| 110 | # Override
|
---|
| 111 |
|
---|
| 112 | def WithAction(self, action: Action, progress: Progress) -> "FrequencyOpponentModel":
|
---|
| 113 | if self._domain == None:
|
---|
| 114 | raise ValueError("domain is not initialized")
|
---|
| 115 |
|
---|
| 116 | if not isinstance(action, Offer):
|
---|
| 117 | return self
|
---|
| 118 |
|
---|
| 119 | bid: Bid = action.getBid()
|
---|
| 120 | newFreqs: Dict[str, Dict[Value, int]
|
---|
| 121 | ] = self.cloneMap(self._bidFrequencies)
|
---|
| 122 | for issue in self._domain.getIssues(): # type:ignore
|
---|
| 123 | freqs: Dict[Value, int] = newFreqs[issue]
|
---|
| 124 | value = bid.getValue(issue)
|
---|
| 125 | if value != None:
|
---|
| 126 |
|
---|
| 127 | """"
|
---|
| 128 | Added by group55:
|
---|
| 129 | If this is the first time the issue is mentioned, we do not do anything.
|
---|
| 130 |
|
---|
| 131 | if the issue is mentioned before, but the value is not changed, we do not do anything.
|
---|
| 132 |
|
---|
| 133 | if the issue is mentioned before and the value is changed, we update the 'bidsChangedFrequency' for
|
---|
| 134 | that issue.
|
---|
| 135 |
|
---|
| 136 | In any case, we do update the 'previousIssueValue' afterwards to now be the current value.
|
---|
| 137 | """
|
---|
| 138 | if self._previousIssueValue[issue] is not None:
|
---|
| 139 | if self._previousIssueValue[issue] is not value:
|
---|
| 140 | self._BidsChangedFrequency[issue] += 1
|
---|
| 141 | self._previousIssueValue[issue] = value
|
---|
| 142 |
|
---|
| 143 | """
|
---|
| 144 | End of Group55 contribution.
|
---|
| 145 | """
|
---|
| 146 |
|
---|
| 147 | oldfreq = 0
|
---|
| 148 | if value in freqs:
|
---|
| 149 | oldfreq = freqs[value]
|
---|
| 150 | freqs[value] = oldfreq + 1 # type:ignore
|
---|
| 151 |
|
---|
| 152 | """
|
---|
| 153 | Added Group55:
|
---|
| 154 | Now that all issues have been processed. We loop through them again to calculate their weights.
|
---|
| 155 |
|
---|
| 156 | First of all, if the total amount in changes is less then the total amount of issues, we keep the default
|
---|
| 157 | weights, which are all equal. This is because the calculation below is non-representative with little data.
|
---|
| 158 | Therefore, this way, at least all issues have had a change to be changed.
|
---|
| 159 |
|
---|
| 160 | After that point, all issues-weights are updated as follows:
|
---|
| 161 | they are 1 - (the frequency of their changes divided by the total amount of changes of all issues). This way
|
---|
| 162 | The more an issue has been changes, the lower the weight. At the end of the loop, all weights will sum up to 1.
|
---|
| 163 | """
|
---|
| 164 | totalAmountOfChanges = sum(self._BidsChangedFrequency.values())
|
---|
| 165 | if totalAmountOfChanges >= len(self._domain.getIssues()):
|
---|
| 166 | for issue in self._domain.getIssues():
|
---|
| 167 | self._issueWeights[issue] = Decimal(
|
---|
| 168 | 1 - (self._BidsChangedFrequency / totalAmountOfChanges))
|
---|
| 169 |
|
---|
| 170 | """
|
---|
| 171 | End of Group55 contribution
|
---|
| 172 | """
|
---|
| 173 |
|
---|
| 174 | return FrequencyOpponentModel(self._domain, newFreqs,
|
---|
| 175 | self._totalBids+1, self._resBid)
|
---|
| 176 |
|
---|
| 177 | def getCounts(self, issue: str) -> Dict[Value, int]:
|
---|
| 178 | '''
|
---|
| 179 | @param issue the issue to get frequency info for
|
---|
| 180 | @return a map containing a map of values and the number of times that
|
---|
| 181 | value was used in previous bids. Values that are possible but not
|
---|
| 182 | in the map have frequency 0.
|
---|
| 183 | '''
|
---|
| 184 | if self._domain == None:
|
---|
| 185 | raise ValueError("domain is not initialized")
|
---|
| 186 | if not issue in self._bidFrequencies:
|
---|
| 187 | return {}
|
---|
| 188 | return dict(self._bidFrequencies.get(issue)) # type:ignore
|
---|
| 189 |
|
---|
| 190 | # Override
|
---|
| 191 | def WithParameters(self, parameters: Parameters) -> OpponentModel:
|
---|
| 192 | return self # ignore parameters
|
---|
| 193 |
|
---|
| 194 | def _getFraction(self, issue: str, value: Value) -> Decimal:
|
---|
| 195 | '''
|
---|
| 196 | @param issue the issue to check
|
---|
| 197 | @param value the value to check
|
---|
| 198 | @return the fraction of the total cases that bids contained given value
|
---|
| 199 | for the issue.
|
---|
| 200 | '''
|
---|
| 201 | if self._totalBids == 0:
|
---|
| 202 | return Decimal(1)
|
---|
| 203 | if not (issue in self._bidFrequencies and value in self._bidFrequencies[issue]):
|
---|
| 204 | return Decimal(0)
|
---|
| 205 | freq: int = self._bidFrequencies[issue][value]
|
---|
| 206 | # type:ignore
|
---|
| 207 | return round(Decimal(freq) / self._totalBids, FrequencyOpponentModel._DECIMALS)
|
---|
| 208 |
|
---|
| 209 | @staticmethod
|
---|
| 210 | def cloneMap(freqs: Dict[str, Dict[Value, int]]) -> Dict[str, Dict[Value, int]]:
|
---|
| 211 | '''
|
---|
| 212 | @param freqs
|
---|
| 213 | @return deep copy of freqs map.
|
---|
| 214 | '''
|
---|
| 215 | map: Dict[str, Dict[Value, int]] = {}
|
---|
| 216 | for issue in freqs:
|
---|
| 217 | map[issue] = dict(freqs[issue])
|
---|
| 218 | return map
|
---|
| 219 |
|
---|
| 220 | # Override
|
---|
| 221 | def getReservationBid(self) -> Optional[Bid]:
|
---|
| 222 | return self._resBid
|
---|
| 223 |
|
---|
| 224 | def __eq__(self, other):
|
---|
| 225 | return isinstance(other, self.__class__) and \
|
---|
| 226 | self._domain == other._domain and \
|
---|
| 227 | self._bidFrequencies == other._bidFrequencies and \
|
---|
| 228 | self._totalBids == other._totalBids and \
|
---|
| 229 | self._resBid == other._resBid
|
---|
| 230 |
|
---|
| 231 | def __hash__(self):
|
---|
| 232 | return HASH((self._domain, self._bidFrequencies, self._totalBids, self._resBid))
|
---|
| 233 | # Override
|
---|
| 234 |
|
---|
| 235 | # Override
|
---|
| 236 | def __repr__(self) -> str:
|
---|
| 237 | return "FrequencyOpponentModel[" + str(self._totalBids) + "," + \
|
---|
| 238 | toStr(self._bidFrequencies) + "]"
|
---|