"""Three hand-specified recommender rules on authored interactions, not training."""
from fractions import Fraction as F
import math,json
FEATURES={'I1':[1,0],'I2':[1,0],'I3':[0,1],'I4':[1,1]}
LIKES={'U0':{'I1'},'U1':{'I1','I3'},'U2':{'I2'}}
def cosine(a,b):
    norm=math.sqrt(sum(x*x for x in a)*sum(x*x for x in b))
    return sum(x*y for x,y in zip(a,b))/norm if norm else None
def jaccard(a,b):return F(len(a&b),len(a|b)) if a|b else None
if __name__=='__main__':
    seen=LIKES['U0'];candidates=sorted(set(FEATURES)-seen)
    profile=[sum(F(FEATURES[i][j]) for i in seen)/len(seen) for j in range(2)]
    content={i:cosine(profile,FEATURES[i]) for i in candidates}
    neighbors={u:jaccard(seen,items) for u,items in LIKES.items() if u!='U0'}
    collab={i:sum((score for u,score in neighbors.items() if i in LIKES[u]),F(0)) for i in candidates}
    hybrid={i:.25*content[i]+.75*float(collab[i]) for i in candidates}
    rank=lambda scores:sorted(scores,key=lambda i:(-scores[i],i))
    assert content['I2']==1 and content['I3']==0 and math.isclose(content['I4'],1/math.sqrt(2))
    assert collab=={'I2':F(0),'I3':F(1,2),'I4':F(0)}
    assert rank(content)==['I2','I4','I3'] and rank(collab)[0]==rank(hybrid)[0]=='I3'
    assert seen.isdisjoint(candidates) and jaccard(set(),set()) is None
    print(json.dumps({'candidates':candidates,'content_scores':content,'user_jaccard':{u:str(v) for u,v in neighbors.items()},'collaborative_unnormalized_scores':{i:str(v) for i,v in collab.items()},'hybrid_weights':{'content':.25,'collaborative':.75},'hybrid_scores':hybrid,'content_rank':rank(content),'collaborative_rank':rank(collab),'hybrid_rank':rank(hybrid),'trained_recommender_or_user_study':False},indent=2))
