From 5988b6a00d3266465273a7c6e3f04e0affb69a88 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <Ibrahim.elseoud@gmail.com> Date: Mon, 10 Apr 2017 19:25:57 +0200 Subject: [PATCH 1/7] Implemented incremental SVD ;however, can read only 1 tuple from file and update each time --- recsys/__init__.pyc | Bin 0 -> 556 bytes recsys/algorithm/__init__.pyc | Bin 0 -> 251 bytes recsys/algorithm/baseclass.py | 6 ++ recsys/algorithm/baseclass.pyc | Bin 0 -> 11549 bytes recsys/algorithm/factorize.py | 126 ++++++++++++++++++++++++++++++- recsys/algorithm/factorize.pyc | Bin 0 -> 25820 bytes recsys/algorithm/matrix.py | 28 +++++-- recsys/algorithm/matrix.pyc | Bin 0 -> 7182 bytes recsys/datamodel/__init__.pyc | Bin 0 -> 226 bytes recsys/datamodel/data.pyc | Bin 0 -> 8343 bytes recsys/evaluation/__init__.pyc | Bin 0 -> 269 bytes recsys/evaluation/baseclass.pyc | Bin 0 -> 4781 bytes recsys/evaluation/prediction.pyc | Bin 0 -> 3802 bytes 13 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 recsys/__init__.pyc create mode 100644 recsys/algorithm/__init__.pyc create mode 100644 recsys/algorithm/baseclass.pyc create mode 100644 recsys/algorithm/factorize.pyc create mode 100644 recsys/algorithm/matrix.pyc create mode 100644 recsys/datamodel/__init__.pyc create mode 100644 recsys/datamodel/data.pyc create mode 100644 recsys/evaluation/__init__.pyc create mode 100644 recsys/evaluation/baseclass.pyc create mode 100644 recsys/evaluation/prediction.pyc diff --git a/recsys/__init__.pyc b/recsys/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6bc57bd3ac78b000f3d61b363cfbd46fbc14d5c GIT binary patch literal 556 zcmY*VO;6k~5S?W|SeCZxPsnjEY@o-gswkD{0R$^?LAhAQ9&KFNF|sE}`*ZmX`~jX5 z1R{xN-kb58%*^!V>ne25_lDk{8T>g;zJ#$v4eW%P3E2Z`4#-ZanUbARGaDjvvU6${ zWEa#NlEt?qdqj3cJKO<2rn~hQl#&HZVw3kdp8-DZrH$Rm82MTKsj>@?o~tolAs$2E z&T^_C5f1qns@rUIs{XPMI#0n*H$SBKf7|Q)9X^_pI^PBD{00@IjB~-F<p@CgT})2e zFfgk{hiKW$3ORn#zSrVnCv(tVoX<DwqLL+yQk@R0lpGGkMgv@QO6eYxB+$~JQcs*R zs=RaW40pLmzYTe`)(H+5NvW?Cmv+I%dKumJ8Fc)W_DHR9k3LMk0<GH^xONXow=UG% zy=ip{+Z6w^kz8MB)3T4L1nN)AuE(9%O1aL7QX6E;h`+Fm-t+GX@UYCq<n#Lq)B%nB literal 0 HcmV?d00001 diff --git a/recsys/algorithm/__init__.pyc b/recsys/algorithm/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89220e34927d884b6c3dce3b3a58b9984843c127 GIT binary patch literal 251 zcmYLEO9}!p49(2o7u<RVH`)t`$bc?Y6h+*W+Nsk@JC!zq<83{I7chRnKwe(n3n9s# zy{@YsdkMctsZJ?lpFjZ|5-0$Q1j-Z+DHJJe1{qn{Bya+QxnYOsNl5g;+|}Z1H%OZc z8x^*Q?PlwIxtV=9=fYaf2PDHE;+9&by;IB_gV4sYRV}p$ZWa7lNsKHPQY-7jTVR(O pwXcRj$yi7BE5ep^18T=OH<dxoM>N-_&12__t5uVPczplZOkOKXJ&^za literal 0 HcmV?d00001 diff --git a/recsys/algorithm/baseclass.py b/recsys/algorithm/baseclass.py index 89656a5..681b805 100644 --- a/recsys/algorithm/baseclass.py +++ b/recsys/algorithm/baseclass.py @@ -36,6 +36,10 @@ def __init__(self): self._matrix_similarity = None #self-similarity matrix (only for the input Matrix rows) self._matrix_and_data_aligned = False #both Matrix and Data contain the same info? + #new for update + self._updateData=Data() + self._singleUpdateMatrix=SparseMatrix() + def __len__(self): return len(self.get_data()) @@ -97,6 +101,8 @@ def load_data(self, filename, force=True, sep='\t', format={'value':0, 'row':1, self._matrix_similarity = None self._data.load(filename, force, sep, format, pickle) + + def save_data(self, filename, pickle=False): """ diff --git a/recsys/algorithm/baseclass.pyc b/recsys/algorithm/baseclass.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16b11748948af3db710aff504e77c4d4593df659 GIT binary patch literal 11549 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 literal 0 HcmV?d00001 diff --git a/recsys/algorithm/factorize.py b/recsys/algorithm/factorize.py index 00a9e61..6d41edb 100644 --- a/recsys/algorithm/factorize.py +++ b/recsys/algorithm/factorize.py @@ -9,6 +9,7 @@ import os import sys import zipfile + try: import divisi2 except: @@ -30,6 +31,10 @@ from recsys.algorithm.matrix import SimilarityMatrix from recsys.algorithm import VERBOSE +from numpy.linalg import inv #for update +import numpy as np +from recsys.datamodel.data import Data + TMPDIR = '/tmp' class SVD(Algorithm): @@ -66,6 +71,9 @@ def __init__(self, filename=None): self._file_row_ids = None self._file_col_ids = None + #Update feature + + def __repr__(self): try: s = '\n'.join(('M\':' + str(self._reconstruct_matrix()), \ @@ -241,7 +249,7 @@ def compute(self, k=100, min_values=None, pre_normalize=None, mean_center=False, :param savefile: path to save the SVD factorization (U, Sigma and V matrices) :type savefile: string """ - super(SVD, self).compute(min_values) + super(SVD, self).compute(min_values) #creates matrix and does squish to not have empty values if VERBOSE: sys.stdout.write('Computing svd k=%s, min_values=%s, pre_normalize=%s, mean_center=%s, post_normalize=%s\n' @@ -352,6 +360,122 @@ def recommend(self, i, n=10, only_unknowns=False, is_row=True): item = self._get_col_reconstructed(i, zeros) return item.top_items(n) + def load_updateDataTuple(self, filename, force=True, sep='\t', format={'value':0, 'row':1, 'col':2}, pickle=False,is_row=True): + """ + Loads a dataset file that contains a tuple + + See params definition in *datamodel.Data.load()* + """ + # nDimension + if force: + self._updateData = Data() + + self._updateData.load(filename, force, sep, format, pickle) + print "reading the new tuple" + if(is_row): + nDimensionLabels=self._V.all_labels() + # print nDimensionLabels + self._singleUpdateMatrix.create(self._updateData.get(),col_labels=nDimensionLabels[0]) + + else: + nDimensionLabels = self._U.all_labels() + # print nDimensionLabels + self._singleUpdateMatrix.create(self._updateData.get(), row_labels=nDimensionLabels[0]) + + # #update the data matrix + print "updating the sparse matrix" + # print "matrix before update:",self._matrix.get().shape + self._matrix.update(self._singleUpdateMatrix) # updating the data matrix for the zeroes , also for saving the data matrix if needed + # print "matrix after update:",self._matrix.get().shape + + def update_sparse_matrix_data(self,squishFactor=10): + #update the data matrix + # print "matrix before update:",self._matrix.get().shape + print "commiting the sparse data matrix by removing empty rows and columns divisi created" + self._matrix.squish(squishFactor) # updating the data matrix for the zeroes ,#NOTE: Intensive so do at end + # print "matrix after update:",self._matrix.get().shape + + def update(self,is_row=True): #update(tuple:denseVector tuple,isRow=True,, + print "type of S",type(self._S) + print "type of U",type(self._U) + print "type of V",type(self._V) + print "type of data",type(self._data) + print "type of matrix",type(self._matrix) + print "type of matrix reconstructed",type(self._matrix_reconstructed) + print "type of matrix similarity",type(self._matrix_similarity) + + print "dimensions of S",self._S.shape + print "dimensions of U",self._U.shape + print "dimensions of V",self._V.shape + + + invS=np.zeros((self._S.shape[0], self._S.shape[0])) + for i in range(self._S.shape[0]): + invS[i, i] = self._S[i]**-1 # creating diagonal matrix and inverting using special property of diagonal matrix + + #if new is row -> V*S^-1 + if is_row: + prodM=self._V.dot(invS) + print "dimension of VxS^-1=", prodM.shape + else: #if new is col -> U*S^-1 + prodM = self._U.dot(invS) + print "dimension of UxS^-1=", prodM.shape + + updateTupleMatrix=self._singleUpdateMatrix.get() + if not is_row: + updateTupleMatrix=updateTupleMatrix.transpose() #transpose + print "dimensions of user",updateTupleMatrix.shape + res=updateTupleMatrix.dot(prodM) + print "type of res=", type(res) + print "dimension of resultant is", res.shape + + if is_row: + #use new value can now be concatinated with U + print "U before adding", self._U.shape + self._U=self._U.concatenate(res) + print "U after adding", self._U.shape + + else: + print "V before adding", self._V.shape + self._V = self._V.concatenate(res) + print "V after adding", self._V.shape + + print "before updating, M=",self._matrix_reconstructed.shape + # Sim. matrix = U \Sigma^2 U^T + self._reconstruct_similarity(post_normalize=False, force=True) + # M' = U S V^t + self._reconstruct_matrix(shifts=self._shifts, force=True) + + print "done updating, M=",self._matrix_reconstructed.shape + + + + + # myFile=open("prodMVSq.dat",'w') + # myFile.truncate() + # + # for i in range(20): + # myFile.write(str(res[0, i])+" ") + # + # myFile.write("\n") + + # # invS = inv(diag_S) + # # print "dimensions of S^-1", invS.shape + # + # + # print "writing s to file" + # myFile=open("invS.dat",'w') + # myFile.truncate() + # # for item in self.invS.tolist(): + # # myFile.write(str(item)) + # # myFile.write("\n") + # myFile.write("dimensions= "+str(invS.shape)) + # myFile.write("\n") + # for i in range(invS.shape[0]): + # myFile.write(str(invS[i,i])) + # myFile.write("\n") + + def centroid(self, ids, is_row=True): points = [] for id in ids: diff --git a/recsys/algorithm/factorize.pyc b/recsys/algorithm/factorize.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ddc6846c4d74c0e30b323e23e17fcfaff1d0441 GIT binary patch literal 25820 zcmeHwU2t62ec!pe_*^VLh%XW%C0<&Rv7pGMNXN1bk&;M)q8JMhb3uYOL~6C%y#Ow- zU%2Nk35bO2hH^4_NivhP(@Zi=W|B!-x07VX51l?WPMjvwx|6h%wwX*EXPUgUe&}@4 zw?4SP-~XJudv^&+wiCk5Od;O0_k5r8Kj-|v&sF+whBNEUzy3l=*}pV?-@v1P(^0Cd z)G9(pl~bzXs8zg}_mo;q#qVjgnpV4GakNLR_NZI6!^&|SmQJfmuc}I_Q*+c8nAW2z zeX828R{K?TK&=j_>Y!R3RMm`H%_ynWt13gPnpLY=6Yo=%VO1Sbt0N}fuPUReI;K{~ zOng994yo$6S{*m>K~<Sh)k(EFY2q2_aZ0UDnfQ>Z^r-5zTAeoWti%tiRg9Y>99ESh zsyd@qXH0xVRi07RqiXf2iI1wvF;#t5tv+kwW2*9;svcLX$4&f@s<^6pLam-Kalo%n zt$sp)G_ESotLh7C^#$ZjsMQx$c~bdD)f)PpQXeYyNU1Nbeo~dEmH$aeJ={v2Rplef zpOw^^R%(t5w48f}>vkVOCZBh!^>VY~&(FKX%6dHvqK#_Cb=`TrTdOy8py%B;i=_w| z9~7gYUUNggq<3|@u+Fp_DK?{xdWfdp(WPSOUiK^1qI<qx;_n-GtHmf^s#jmjWP<PF zZz+e3qoRHU<=|eRgLAnaCiEhx)Qe>uF<SMDwTOF7H>>hnE7nZK3=)ILude%1<cICX zwpc}<YbS)oS~;jiCV_3%>S5Ky*jF(Oi@PQ|faqFSudW3Z-=qv7Wx=m$f5j&dx)K+Q zQ5fu)L=NYju<VC^dD*v}4I=Gg3{_KX3`xsDHK-H;>fK}m1jNb}@6tQVS4>gPI;h<< z5srJI7!@^N&P3IQ0AqP&p@g{nvHm4IdKhr^aUiiv5aRe9<8Jy8$FhgZ{~;24dEEHY zrviY#M{@g>Kfr(GUiBf)?I0^M+9%PBD)&oMNF0)MG&`U^#BR+>`k*v2Ebke4AF<`j zL(;;iDre)m!|FqzgE47mL<MP8M(&t;rF=-0$MG}4t^SJb2z$L(ts(~<O-sw;QW3iy zlbVqCBl13}$}_6`jHDcuE~b>XoO_mFSpitMv)ya84IjYQE|=Q9ThzW=suZ<$TT7L9 zO~J^;er+@(U%P;`n_CWQ>rJejyHcz)eRsiUc3lTN7;`QTN72P5$ChwUE}qQ9Z4@iI z?l!{uy`YSSaDM95s$VP1al%2Wm67R}gA!ZOZd0QZH`?%}WKBn5vlRK|cEhu_<@rW2 zELL6a)mpLY&%4))(S{q<F^JWmA?c?u{#sNFYHYHI;Z;JvSl*3iRW`G_dCFZ5)~m%+ zGM1G&cdZ_F&V*2#k9He=qBjf-t;!rrDZrNx_vU57x``mdKSw<TmP)D36mb<1=Iu0- z3Oy3`O4!GJ%0%oh^D>~KK~`X5Mm=;?Sm68S5Z@m;%Bcy-VK%meDTj@Rla=EzmU3x^ zOG~*Cj)R9>HyeKFO)$zsUMK)2QK29xW~pBDBi1Y2G(XGcXGL&RL2m?Wk&dQVz^rb8 zYuH*DZ<MW$C$e?xd`H9zL}mqGS-%nuvlLgX5Z3P(0^qoIN~vC9ip)U9rTxm9j5c24 z9BL~G^a|e2Y}Bj%nc!|%+z6^?uJ4vMiedG7Sl<My(q}FeOB;Tr9x~xfV;6|IcG}?X zGxn#2@6W7R4DaU~yB;Tc4v*%PRBEF4NNO-O*pp55Ib-QTr$nqxXc6DYV*f+D?5+~W zY+_kbcGb<2W3dhc)tHT7k~VSJd(@2N^mgU|KiQnMG}n300fXSG9xUNjMuiW_DsUh4 zsm*@w59E_mz@}Kv^<flCnuE(|F8|`}ye9N7x&XDCyE!)>Fw>?1*luoRZa&A|Akf~d zgGR6-Hg?2~l3ZoG@UoS{V0|N!Diwb%!bVl<29Cy+Fsz4Cpn#)YwA(E78)2a!YwEcu z2|%ESgIUNqbhXItcnXx0c4nMsoLOhsnQ-L9W|2y$$s+y^roKDFL%WGX3)0~zoPtM= zvzL-J0OZ?wDJ*ItpKAcnJ;H*d)y`|`Ar3s2QkHPx&S|xW)83;tdszZ&hc)Kf;_!nE z^{SbNKnZy3Q#0qcZ>n3nlVl2z2ztX^ve$<J3=5YyzzBQWJ;rm)en6#wDg-MSY;mi0 z6k~NF;0j~|Jq#)wGT<OK)vwkuNTF$agKFoxdWhT+^$@!Wv+h$<TS|qOoM@Cuh$DN_ z{9vGmCr_{qAfy4cIi?<FSjj;V<b4LsZT}4he~~@dfk^>Qc!(tz?mVaVFsdP7Glw%A zvLHd;dnvVDaFBbe_8MpU<Tj4PZTza!YJjzWm9=Fd4sm?IN61FYAft~A%FR3uxhzZI zA0ao1zPlio{eB_$F!cjM?sTdXa#%26lYJof3#ok|_Y0~0AlKbS3v%B|bwKW0to@OY zn>5+24L6bi+Y}*8_)U|A;ldy0Hs9(~;m@Vpdvw%RX{U@mmU76Ztef?kR-kgkc1Oq( z@daH>$f;B*L4#m*Y#b8QByb>6xBeXjLY7Ij-zFn1WIh5LtU=j9!pO$wVybV5zAKci z<ZDoWaz)@Fu3Z;uZ`c^&0J<5DhGjU&tdKvlyia#e?Y8Dx<N640f?WySELJM@`(R~W z`4M4y^B)9_i1^C8a&cktilF*I!RcUOEoR*czyv5w@|3@+iQDs*&|1nMIV&c|5?oVy zS(DYz8)DB&9dQ)Zq||YKRF8@k8I0u)$YTJwC4WhTE8!e4UQ%v($28vLdk@Op2%F}G zJfEc08-7hl2brpdd23XHNazvBl1Hi`CCR^ehY|Bg+oWlK2LMMy1kH`b>kHRBsRpjl z<CG-5*gy|uj~L(MJiKWJGYpO}pg<@Ti=1R7(2@57Q`k`l8EkVlIcdv90q+EhrV(&~ z$Q9HZ!pa&N8GyOstVtBT(=5t?nyn(G?}=okg)#;b-ZS$q?;H%Bq;x=jN^|e1e$vUb z^K5F+nRce|q@77TnZdMk48KR5KKwW190wg8LHr_~S5wCkenf8S2pFev`R}>Zj8h`8 zxPC3_`}^3JAg|%GWDvk6kw+3TJPkZZZWkaVQ6}L9SxpI{eM8WVP}PMy=hSVa_6S)` zOMOsW5U?I0rYPj7HKBndWNdW%g1QAFyE#B~<v?r#X$3PwJR@`qWRJw~w`_{xy&>>P z<(l9;6a<0gxNNemFPPWX|DRHS*z)cm#>x6fHuY^XJ0FRQW=v70jjC`<Fdl@&AvI&i zl1&zXA|_5B=|~4#Vat!&zJXPR6+j3w4yhC1$cF#av2j7e5{Eo82Fl(~L>wJbo4~?I z!qSW@aEOmm4mb(&0!W!?Ee}M{DS$NPM3ZWBDo!TEcP<f7WeC!P3OIh@&RGSnA=v<U ziNho(6n+v7Fj<!De>t}-IPRx*u82Gg8GG~xZ0+^bElXjhRrpJ(_CO4&!9=EI9JbJ5 z6@I<5&|%WPGU;7PB(fv1YU$U2uUe>nxp>dY&)tOKi6qDfvEMrZ(&$1&=8vF4PwK2S z&W|BUR@}C@4cao7=sP!;6tPJNgW4KI@VoV9)EZIeyz}mr{CeJfFuOH7@1A}6<x}o# zHK-NtQKg`f^l2nOAyA-}s#pm=@R3FlmF1w>DwIGg{1AzxBP?6jQAc5>?{!V9HEa&L zm8Xf|u|8A}=zGvHP)MZ;;YFR%#N^P|)z*S_!OHgy#WjR9AR!He->}5iQdap{7K+?K zK|xWC{FNlL`;yfbu{ST^L%f&JnISVAR52`(OnIm9-bv$y;9Ii3gfX}(C*+MvH=!xe zTpFq!m9aoVXzxQ6<a<BD5+vf(C~EH$c=Mi*(@K@P_Ptk`Mt~V|)`SX<_&chT$nQr> zL2>nc=u^FyS@SFd%GIJ=z?!jg=vV9aeD68t9Y>HORS-c@rt7_ds0?eZfEAP~W+f$2 zwhROvh<O*kP*}Ng`NlimviCaca2eJbBAkLhfC3Xd-+@q5;oNv0jmG1bF%JDE0yWs@ z98OJua-MWDP$f(uHfh7j^kC`;DC<$AyP&H_($6@D($kPur$9IPOZs`tf%1WrlMk2b zG9LXF0^&qbhG#YKa7u+2Nk$(@Mj<8HLQ+{y6i*OE4S59)C+1=X_2Puu668UpIuDg7 z7eoUDWpk_qYV$&(0rHNanygR>Od7V7aB19rqKz_(`Yxj08K&WR`l_7Q(8;&($SF~f z`?Jm`+A!n>6c~ceTNq|F<XQl*Ou7}mVYv~4M`V(2=%q=N3rcO5pIFigpHRBTeONGi zdGsoP^^W4r!=q#`CzNY#7|Qud0Fspt5Rl}C{|b+2kCZb87Vu>11xKWf5^)llYW@m5 z{|e*}+#L2Gz+<$Z04gaskWlz85eRl5#arR=kVh2=c4n_y>s7+?Ax$R>n(Eu{#$fEX zj2oB}sLXGJ?}Kh+kje=NvuG;(Ln6HJ7ZZJnI!1OoDucySQmsEx4a^9X8$$}pH?AwB zVyqcjS#+q4=}}wQ8ULL;5V$r}N8j5f3+g#!^*GD|^Owv57m2Lnk_EXrnbj{@^O6Pm zIL?Z|lYy^F3m|?R8a@mH?Lw^~x{BLoa3T>GaNIFa!;C@_#{_V)Gb%ES3-Tb1gJa~& zP%;D3`L~?_8Yu9tS@jB_4gT>J&T<4l&|Qt<2TcvCSN6c6qWTe02VveR;m5%kF6XEq z)vu)!yw2sMxd2kOm55;tIOWbGvN8YuqI<!;>E0HNySwt<jrTJxa%@?Qt$DZBtlsrQ zx4s4@1GWNUZ{02?ns}6gWY|tL!g-sMn0N7>Pqb8n0{8<ztZO$1{XSzpNZ77x7>_yV z0d_@IicFADx7yT^y8%Sr?n3%a_OQ<YIye_hBxYhTSQt_)K=SVOy4Jznif?;&iy;`e zwV=Fqieu0;TvQN31(5IRzpI0U86u&hNH9wCu3?yBt4eYlZmzfmlMT2kS2ypXDag?P zb+?+LqqR%*dIc5QqcVW+BpBq_EcfR6_x#X8b}iln@dkLG%+_7sExJwEBU~d2=iM6{ zO-vquaN9)2%^Ay33HGL%kA0z|Vy)!Q!Aul2Ll}<i{(ATH+5F2c?7H~5^L~4nvUuI2 ze8OZ%zsc3iyRnL=gTs!^1s|hH>gv1QXxEzQTST04a$-hw>%xlwKzpl6v}5H>fi^#E zN_4QoNXh_-D)sKIi{8?;rK@)mExLhrwGSS>ZkcN?ve_fW$kni{xNFVY78b6{URS01 z-=#vO7@o+86+O{@dqC`foOqs`b>6OHE}zfOVH9k_teXpa_N>mk*VfSKDT)j=w_KMx zG%mvS$U1;UyX6aS!lLNUWrPQmLn9n1&yBqB=m@uq)1!YJ^S>VYr`re7FpzHUMd<o4 z##)_L&Jfd#+DDB?=?Q=PoZNYLR)#SPX@sNTF9$K}0+-BU6tiB7@e#6b^g!fw+e;jg zgmx^ka07{XFNuEu%z8^X(OgZUf-&il@>*zl<n_HDW$<|hZzB*jQ*6Qkx>n%gj0VhS z5mA28h}VKq!9aDSBTeAjnIw?rxZ5=`TVa94LTqe8>=ApQi<$AroQXgnx;78~Gb+cn z45xUSZzTrQN_jF+0#USd@lo<FGcV8NK?E3~u~te9gR?P82%@nxagizxh5=ne&)>nL z&m&NWrorH6ox|XsrqiS8aZ#mBfbpM7_w{C-(G(u!%ackwW2qCV>C_DB%(a<Tvgn+( zK8XNyJA6jSGbpt1Qed)>II~QWok&|2CyFIwha<b$f)^#pa)PnE!c0mRLMjggO5j$& z#89%}0w&mBzkCXuq$Z!P0ISKT0|oArE+~k+zv+R8Js3fy4FL|O0|IBeAi$*|KzOKR zAMj+D)z;JUB9dP$)nN~SyOvEsttmJ@@Uxf68-u;WyGbOA37%OPZl=RPDTR@ay?_*K zE3<|4Ut>Hi!Un`jeTo)MkML1s*<wbo@E|y~x%d7l>(bFNt(Y^6%f5HJ5rWf8@P>u0 z{kg+nUKoR*<}S8*N_6GkE|u%iUN^-@@uns#8M;($xIwu!ddqHZf<pqo*WI8r=VJWr z^1?!(xHN<YrQydLT)ehaSh@K2%`27#HGpCJZqBFb3$Gz~huXS+_iR4@={xR)*W9z+ zOjn|nHF!`w#_5X6!}e0#$s0yeX7LfdeEP9os5mjLboP>vOo_yV7D4eA?wYeA^tD>t zF~8u0xi6&!Q_)4#yTaf;0>L(|37J^jF)XG+_qdV#vQYyb4B8>p1i@v;uke(Sa;o7$ zC}5_5ZHS%6oq$BT#L2OA>(KoXBKsdYm?&_{)*;Nn_<*x^Rm#E6fHS3_X1~f41-Ta| z%F1>)ct0gYqmH69UPv+P>H)*Q=@YUc&U0UUL_a5Wf4j3T1i^jliZfBGE)F(L10q{5 z3k}de0s7L~@LtUNY1#5P=5(%-T~85;FD9)f`Ey7i2l=->=%OAD*4&f9Nit|fIq!56 zJ6W$)b_?K2YxVmzEty6K4ACK$0;q!C0Dp-iZv?v}MzUCjh{<A$7+Kwh@e-^{Ve`SF zL0cDYi*yJU85p1i5sNY`4gw5f%+W3(NaEwMPAw_q5@Jc8IKbJ=r>j|1+DGK<fXY1F z@G9imDHK7f5?IBc0OElt{(6icsOlB|7uX0Mc6WCLk8@eO(L`4lPY{~|O$v@NYRFCW z&EpE1<>P)S5?XVwcg-oT)ZL~IamE&nHz697)i^;hVYl9N=~9LfLv9s|hp6qDUV<<Y zabpS;T3h9U*(S|e#fO_C<q@N!v^+7480evd_4mt5#;{8s$s^)3d@|0n@-`S$8E6C% z4ZviQ3Pg*6ue~O-?lIV5J~`5;-YAGhJxBCmI9p;xVK8dK@^^q%^rr?QBFeDt0kR>& zM8|vaZQ{gZ7_1s4PNdm)8oVigPdHQHN>2bk8k-1A4VWEm$YSFVqJW(sMiPl&;YCe2 zfFk5xF5}Vv1_3$PDJlye334kKb(e6iEye|C6dVb;kX~`2y!sX}Sh(GR@t}(%J{GMa zUtro44>_P?W3z-O6;$2lq!dh+f{mY83B8G-Q!2q=j2_YEAk~JZ7wR(Mu(d49s^7;% zTD#hKoWMFpXJrW3)FZSJ$g=O7v!UHGR18ph&>4m2lI6wzlwZJGo;y4@ce2&2rmczI z4@O4!-I{;jjhYSkR0xRz-&iT$^($J|Lej`hn>0-kuh~EsaczwQic@0rb4xi&NFLRB zLgx!o)aN*47ji73fR<*7CQY-(O^`Uh;@@QJzB#Yn9X2P&MT`JEJ+iY+yW(?b&cX>6 zq&+}(iS~_2$$05VjYd$~f_3RDEJFHRTZr|6Z_8{ChEt%VO&}xybAzDY;t`?rqWb$F zz8!OVaX3a(pG=RYo=^9tN)*=ON47lWb`XEFpJbaFI_*HnwRQnbO906MwuW_twu=P- zxBRPttHpN4B1Vmg)i%!5IBU%+^kL>wfom{b*@%9vmC)PGKyM`cGKtMhqPgyHFn}dI z%%&)qNyf${&h;k=-5!)s>0aZpNhC^MOki0DWC=?k4|C#iD6hVS10SadUxSo%D@9BI zNa<;%h$|*iaK>#rBJ3cguayE;OIq%4rHH#E$_%Kh7TMtKEojRn3R{~*GbSoNCRg7A zJ+SBlxCw3$@Z)IJWyE-=z}s}^b3BC|dFX%P&Ivlbm}FqY8R+dNfIW#E({yb)zx^FJ zw7_ch8SxZBuTb0z2;db1?fzBZP#IV4IcWu>Ocoev7hoUoOfl99h<}dKyAF9pW2ERM zBMJk-RVHzw{6$j&N^rd}4G1h_upL54I4ny^4DUM*+((jdVs{t}3~y2cQP?cuP?9WD z`xIR*B*lW0weXs(1?$CBM;Z8UNR_yERKX>R!*mYC<siDSz;S~O$+Zc-z=qPwz%`0w zZ8@Q23zoD{cHRbnu)Ew5yV((2(L7bc5Ym!?EiJCdae_g<LSj3UaI_Qt^_qvIt3X>V zt|>mXjU;ABkEh+-r`(E=3b~7<iQ!tUu#uIW<@ZjXz3_O(O`Ab#(W;?YMdX3xj6+{v zkkPlwu^3XD73l6GFkLdLo3H=?<--b4CUp}S-E@m<@bZY0IH8q&imh}On?Nz!mS7dM zcFJA6(4~FVhd6Y2l(@-4IpT34cZ9WZ3l!=lx>lk?kJ!@~Ymc!kF_GRVHq7OTT0<B$ zNrkH^)LCidLnYz;46BhE8kUfLsIWu$LTa(jCD6?p4dzscN_Ek1Csd=P-U$X$h#~PJ zE=MhkkS(=v!=}6lH?0_>3WYKR*)U}O0r}nkEQavY#NBV<MTys+@%=-X8U+hEnx1o> zb&jVgV2`H8oh(v8H%0Y{Fx7|nlrx@YIGP$2C8&5l5dRaZg8!*lz>3i2{w*S@g*$JE zr#(2zykIz-X9&s$Edgm6A7WsdL?TcpE&GD=o$X<D3mX7TB$9!0DZvAEz%5YlJzbY2 zeJa+b5)T-yHU$FX0&|{kc#M3oENqrt1TwxL&hTXMI0<p6X)mj+5$?YjUWa8Hypf^B zl_e;fC1#4q4vPmvp9eT($~X0({B16&=t!_wln17;bce8Pup?tHa!R5oA!2Cp_`@L5 zB&@hC0fhvN`e*5zgSi6RB1(q}n4m>JNmiYUm0HGJ!yDun!NZeOqO>S6LFHn9P>}GJ znq}w5%<^BOMIKFrKPT%39SC*+Tr@#U&HaY7a<Qz=_Ie^eyKnw2h-QWy!LNW9Bn)YS zogWlzz`A0#%8<-L?SrcnAV=6Gaw7r^1iWi*BVBwFYDeQ-5I1OmFrq{*I0@L<ad;nL zLl3K+KcO4K!kurbJ@gG)jJ+#P_xekYDTG9%oTtCS2p9>8&42+pj}sBcGU~il85tk| z%2~Nlf^F>X!CAjmYi17v2e9ByfESQ?j2iw1Ha`OoQc)<e`olxkkJd?MgMbbxI0%6$ z0ffPA#2yH^f0Bg@X;y824!)biU~)&WJ@hxRms+CD=TFnf1p&MQlRIuE)}SK2CQ`h1 zw@ltkFS$8fI%>?HIpf#z_k*pV0li%@Uk}&MFm}ea@#48l_%}yIRcWIRBr6&X5wnUF zF@$p`3$>!6$o7aUys@$GvK_+*aM>)n^aLl#y0!qP0Uxexph)1esZ7Q})^qK8v0|u< z`@(T8mZaVTMWJ9ouN;+Wzt!J<E)T6y#e=eiXyF6Z0#2BBH_&#K9t(c34vVQ(by4w= zllu!$izIY7MjHbk1L|i`JE5Q{Lyy*;s0a~u%=$4^O}l|^WsMj#1#{`=vA&mE#%#NO zr(9aJ<zgiP0mF3gk5NvL8cd);B$hmp_;_2nUKQ5F`<E!={WBI!BiQnHje3D(Af#J? z_H!iBB9`I;HLfJmCBVq6bmAvhV=SaZNakYneg;xcu(nY9HYOMB{wkYi<-(Q>lds;V zn8-nF6m<ckGWJ#Dm>`{N+=TOp6@`e4pN!`+`Wp;r+O$G@yr5#$jKM2;RPSGZ=i1VZ zWdVTq&sp<duqHb;E*)AJiWol|k4ht%8?HdGx7l#oR5KCpt89$G6&@t2i%%_2#(_{3 z?`!D!e~_BY<3(jhQ=b4ec@~eMD{krpn1FHEMW0I@MH;nFqj*Sdp24Mqaa=xl0l!Oz zW#cRhVwJ#sR_a0ubiKrtenbhYLZMtQ6$(a?M=gps$AGsU#OQ1oEwM(wz_edsu*N_X zder54UuN)=4E_;=f5L!lj@hA{PaAUb_B1c3PT1}=hua6g((KW}!NF_U*GBtB2aaCM z_GWuWr^k<<K9=pz{#bTs{7uoE03$2`S!}>FI|OPhmrz;$xKA(P(FB2yJMlo^2o0IS zJ8sP5HauiVyr<=sxfn2fNP2j|Kir<jS1bB?(VWLa4}#_kg1gLH<w42q=+|vv6XL@4 zlW&p(`*++>r;1lDs2g<qeHk4gC_zu-7Vm+k<nD9+&~)Rf`kD+7sw^Q8)*2UUz^ibh zi9L^uR3ms?6uEQsP6GaHOJJwmlZfPC>zosl1U`Wf+=Fsj@Gz+i#&1S$@i;YtG&uhd zZ>-yqEO)rt*(tex#fQ%wthzawF{z!1c0yL+tc6HfRF>9wW~MFxHFpTuY;3161~j7Y z__@qI@;n0Yg<uLTBPa|WxihkQIJ6MvsS*R0z;*T>ga(;^gKs><LJ<nfTM5^N6HLb5 zQ%FVl^xca&j@QwlQBtL*(<Kg+bc4S#Pof5LR>A^<@4_LPBzJ}5gis_6h#5OSLB18I z>-fT*59A^f+)}|viUjirAz&gJ@EO#aG3+Le9X(4QF{drVK$Jl<pJmH9pJ}jO#%?Id zpJ^q3*2p$!jcuG(*SAkA{JX_oz#J~;h!mD&?E<_Rwf;Ka_-DRTLfmS%jG*1NYq(ef zWPuv>Mvn0CTn1MdP;B%5Edt}oV?GsN$x{<<Sh(%?`z-PYEJA!@PVIrLsC>o1&Z$s~ zv88^KYfK?rIrQLxBfs-W(4&kz8HlX|j#xLy+O}4H8e12z)JE(SIn{FG*z+RrAQ#@@ zkXZm&tTwg=M2MX_rIm#{>#_}uJF{#6ZUYEHVEB!+0gW4&Yb@Y^3L9#BUd+Q_(}(0P zHWo7cdAVDKod5wSi)}~IXFg67MxF6n5LdGLYfR6E|B4`AX5~L7qk%NkLV+m=B{c{r z*prfkD0W)-zp2uo8~_0t+ZkmrXV_-44t507DFfMb>NMqIoV7H6HJuOv{tj_EndTn^ zueW)5nx11)adN)Js<UD1u5x>$7(EF03J;pW9$Z##ufcKl!EJn@r|@9Yy$x&5@_P@q z_Hx0i&AB~N3SdZg6P~K5u(`MQ@g<WcXP9*S?`05_p>j_egXwwhiI!{(`TtdKnob(E z)uv96rV_-+uFUd#TL8P;#8kOn{Ck`xNve^F;&rAEka(LKRv}KN@Yl7Oy6x9-(RsI9 zD6&Mh<@fUU*&V2b`>lOkQ1pKQzVrT&!3qN^P>m!-BxrP8B*)$d3?485MN^^ycp^cF z!RhU>{FfN~I|L%A;3O3+t0tF}c;GCy#<!$|VtC(VVM+n*!rnhX^g!r`$O$W?7I}23 zj?mi^zMY^L^zzK&H-l#~btpCB^nj8~z~eLaG+Hud(XXSP_qz;!kAc9He1<2$KF7B& zBWO=Y-kzqo@eOqMDb8sCFMUH$)b?k`%+>fpw@~qUsPSthl#@ShgG+cc$U>)N0S#GF zvVd3ti32>!prTj-fdUc-EbyX8`w-u_0(HbYKE#E0yqIGB@pri-g$3|AE__vMh?0nG zfnyl9;DN^kicXP4qzioXO3HR-iAg?oVf;v`MJiu{&A-eqVa*ydYW&r>Mc=n>lD2t~ z!FY>y5yp@JHwt_RCep;^dIdr}+>s)P5$>Bo#puM`MS8i+y16g<8_jaj6;K-;kJXGd zns+NfDG&P!T=PpC`C=sBn>+KRd-V!*BU`&?@F{||O0aWgt6YBhvs+xNe51Vf|Amt- z;p95D>*K<S;k#(5r+fl--MnDenipdyFz-+rL&0m_u`k<_47a67GIE=8-{T|VjU0R< z<197>pB3aL02OqJD7BDA9Yrp?RYdecCWaDSB9buQZT#^xQ3ny%FA;UnDyUL3xaZ3M zvc{T^4DpV*j9)#BfNU{l0jn9^jhU^#f<R6MY4&n_hGI6X+I*klI1I5+29i$W-cDFt z_pN(vo29d)xY~q!-2O$`KK~52z=ECru;y!x-k)GR39ku`vD!QkV2UgRt6`zq{3=1p zu2sL7#d`3@r~s1ptSLC+A1p2+cVOr0SxFS@mq`Rp#{QxVLSQH4?Ei`eq%)&b_Wl%c zkIfq~++}te0dy+HR>iv2gMk5eNkM?X>p)oXuMrd^ObRMOlF|}K%7;J^fE>e%0f;d* z(GVq{QKOpyZ#{D|nkv1=H(I=(j0?AA26+Y_+ggCeyy%iTtK=X|n2#60N7b^J+C6Ct z<$}4B2{pYK`K6)Gy(eHcANBM80^>9q%m0kG_Ez-%oKZYaUF{)Wi>);vF#a_cfiTB@ zeEQ%g&imnl|5q?m@4q1U;e(&R_}v8fx$CJk;hvV=Pj!pg{rn0Y^btoT3SC2wNb5KQ zj_42qNrycgHdx?}UaOtbH#*b%m`>aQ?Eq8@JWKosza=CB(NKfrsPk<M{wmPJ_j0hr zux3q9@L!wDKeonjs%|?IW8y5;YT4lJkN3NNn&;F~nb@N_VP*Tl-Jq>lK%{Iiu}B0I z$Bcue=G#36JbB%hFNhS{NV@FtV+aR>B#4t^B=PULLFpEth6aJ8v-pp<E2i*YiJO&d zLN6ZKuwTS}`JVJ<@Xo+K#g3y&6mMyD#5(lK7L>GJq(NsTnykanu2Iv5?+;<W4ym25 zh>kQxrR%O}+p<(uu_>sn5Tcgxkb?>fW_WyQC@m-D*VT4IcKe8;6<zdNpW$Z*PzPPp zNRBsl4P!bQ8B>R8O@|TQD9cdikUC=YaSiC-hM<4@Kg`dD-=*xjOjVir2GtNWZ%`nj z-Z&b8d459eP4bMv%MT9d59OUIHhiKFV1r8T9fxiSNH;1uNv8t?K7&X)PyC?2lujJU z{Z(%#7p+#mb--Ye$A-T9x0i3oWnKP$WE}f7V%EE|-87f3Kjr2$ZtT)|`dt3p+<XFA zz^x9S8R$~c-8H(pm-f}wQDr2WI$ZtO+;ekATjrj2S!;7|u3KZ*S)MoC?!z-6@u2`S z1luR{gVm<{5*jmMkkG>Yok(s3OpJPxm*cR6-#>orxDB_M+w5)u`7{Q7#C^J_tFZff ztxankrj|NQ92%B#^lvm~4^i_7%S{Hu6TDAO?_>0AT`D$W)csU!-C8x$jJr)Sn%xbA ztH0X}ygy>?KW6X;2qakto8EtnBVlvXRN1suHo*-*#%)$NVUd%x5X~H{+U+k%WM08A z)Qcc7sj1#cTo}&u7;}wLzES;#G-pQCZ(~%(K?!Fr`dPi^*HM38_1m8@L)34=fq$FP zzhv;=7<?H)o36^+)0C_K6T16p&X?|ZeZx@6jSLQs_GJgNgQHWqIV5_&$G~IoqYPeV zfXA$6NteX+2A^5M2d^o=3HM-Bwj>cS^zbv55T^0bfAM4y9|Bzp8oPPxpOnA1?U8zW zzsZ0bK(0iYGEg|-^V%Mpk)9;g6H}n5K;*&+wE}X7#Qy&e;Laug{{TvO9!0%~MmIP^ z<FjS?*4Mi8q(P#w^F~<a@P1{0f$<r7^J#Jdsk;&J0+R6x4Dxoo^q%#8k*)Fs*-CWW zHaBzlCf8)Vc@~^t@C^oBkb_%)S)KF7$>lYISR82#W0mEH1MoG@Nqp&QEH#FA{MY%5 SSQcr0sp;>k@2cVHcmFTQxdQh9 literal 0 HcmV?d00001 diff --git a/recsys/algorithm/matrix.py b/recsys/algorithm/matrix.py index 9a8fa53..85da209 100644 --- a/recsys/algorithm/matrix.py +++ b/recsys/algorithm/matrix.py @@ -73,12 +73,30 @@ def get_col_len(self): class SparseMatrix(Matrix): def __init__(self): super(SparseMatrix, self).__init__() + self._values=None + self._rows=None + self._cols=None + +#`nrows` and `ncols` specify the shape the resulting +#matrix should have, in case it is larger than the largest index. + def create(self, data,row_labels=None, col_labels=None): + self._values = map(itemgetter(0), data) + self._rows = map(itemgetter(1), data) + self._cols = map(itemgetter(2), data) + self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols,row_labels, col_labels) + + def update(self, matrix): + + self._values.extend(matrix._values) + self._rows.extend(matrix._rows) + self._cols.extend(matrix._cols) + + self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols) + + def squish(self,squishFactor): + self._matrix=self._matrix.squish(squishFactor) + - def create(self, data): - values = map(itemgetter(0), data) - rows = map(itemgetter(1), data) - cols = map(itemgetter(2), data) - self._matrix = divisiSparseMatrix.from_named_lists(values, rows, cols) def empty(self): return not self._matrix or not self._matrix.values() diff --git a/recsys/algorithm/matrix.pyc b/recsys/algorithm/matrix.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c5cba451931863960257260cb5c4e1e2c7a9d74 GIT binary patch literal 7182 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 Ge)iwQG9Op~ literal 0 HcmV?d00001 diff --git a/recsys/datamodel/__init__.pyc b/recsys/datamodel/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d3822860cf95760a3f628de56e2c4b7be24950d GIT binary patch literal 226 zcmYLDK?(vf4D4D#5j^>cUbG(&5g#B6B8Znz+h8m0R@&}ipX(d^fa$VeNG6kENW#DC zRp>r9&8Fbgk5qX>U?>}AX6(t?oj7L~F(<uLFsi7LxPoCMea3VaO*JeC0F?Itf++h# zzS1ad3@F_rDeHo42W?dfZHh0{CCii27JW=rknf?`*liQ)JY+ewdOJjeUIMtz6@VT2 Qmb7_Ka_{j_zsOYV2d&&UHvj+t literal 0 HcmV?d00001 diff --git a/recsys/datamodel/data.pyc b/recsys/datamodel/data.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57a97e63b45c10a3996f3fd0a234824a80e45c99 GIT binary patch literal 8343 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 S$duvH5qV6NW`J5|O8){sGDSuJ literal 0 HcmV?d00001 diff --git a/recsys/evaluation/__init__.pyc b/recsys/evaluation/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c3acb92feecc6b7ce41ead6f886938ec678ce78 GIT binary patch literal 269 zcmYL@U1~xx5QXP@s}`jTa0mCL%?3o;B7G_<{iFCY#3Yz$^CL+V?s8p2H_%BDIxyeN znK=)q{kfU=aQ;+qsg~zGcb+jcz?Rv79)NCP-@=ynD|qdO{G}12Tvwk_lqrTXVR1-C zlPXn?Stc32DePC>pt`4wm-YK<iBHe#hqpfmqI3?i;}`rRf3OrbdLw9;q$Mvly|Oa- zO^RPel`I~mvc|<!58}`l8^difD)(7jhB--hlr@V$q(BAnj*T_*+(&(MX3_ATYqnAI E3qL|aQUCw| literal 0 HcmV?d00001 diff --git a/recsys/evaluation/baseclass.pyc b/recsys/evaluation/baseclass.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa9dc9bb8e2b08d3939d210755c6ca6b9c77ad9b GIT binary patch literal 4781 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 Yw>;|_Pr-Y;;k<AYp~b<r<L&r=0va5U;Q#;t literal 0 HcmV?d00001 diff --git a/recsys/evaluation/prediction.pyc b/recsys/evaluation/prediction.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4631a7f19c45d8d8446fa8521b00acfb5fd92c82 GIT binary patch literal 3802 zcmdUx>u=md5WvUwJ(5d818LKwmB3IT9a6}tq7qUSDiRX<g<PY((8oz&ZR}0X!H<h~ zui{YhrHK#{U-%Q@Z{RBu%*^^8E$z29P$xULFRy2IW`8^0p9_sN`{Qv4<>TS+mss@A zC?foKAOT1n=sMs~+l9o1)Pt^P+a4qpNUPAT+I9t!8l-jT*0Eg$Rfj_dq8eNPoC7@R z&OlTj_0EiYXCay$^)|-64T$ET)0{_D;=l1}I#@FmQtzZNwl#yTzLJH`v%>bwVUKw5 zZF@WT`kM!v`?j-zojXruGLR<DGec(XZtgs1_7?W(Pf#czYth)Q3z0`#aDnz$xQ<O1 zE?hEKbegQnFZfEs$7ff`%-=lFc``7{-zkc`Xz;+@zAR+wN7Bf<zVywYpC~^>HDsK{ z*^zI0%D-RagDmp*i-GC!%r#$Pn}+t4Hr!<wGsC_r*V;&8ZPt8}_joXLQ5h;^;8$4m z4HO3d9XLe~xNz*ksRQTMn;qVqc9UkCDor2sRUybMLmL|esGtdgCM{_<UL}WIA!C}@ z3oP_wthIW1s#@_uA$xJ!+8c&FS)_YKeyl>HTU#>hsU$C`qtzdpUY>ngs8A1etNc-8 z|FrsrisF!5wca0!8ZLeXizYR|2}xbjmipPq-?4&L6}RBjC0QDbHoLHbwi=gA@1Wap z6OJonep#<l{RbEH9+(=r8k^4?$k5z6?LoO|vlP4zy8g_AAKPFsGQH6fyc{mS{oNfn ztCU|jt-$IT@A+{RxPkkQuVB|$tIu(<lV|YAYHQ|rf>n{#x`w)trdD|lwzj^5rk1f{ zI94c1#u1{G(SA=p#Q=#94^^Qu<Lme*r9V8v_xOoP@|j|fbJ+2DkOx|@pJ{s&d~*SR zgCm|0@VEGdz+gDy1^_(7<D@vu1$o1UO&x1tp7!ww*!5+0WNU{>E=`kX(j?ppfC4ui zWkCLBCG;TWf%vjwi{k1%EP5FQIP=bR_kHJzx8&5E51a-3TXEV=QnEY{nM_p>u>C=h z=FuRbCK(h2Q66HQPER|}6o;*#^(VGm!eOD$&tTD{bgfo%<~>$P>~>xc?|#K~88#x% zjfGz4WFU*T4|z6hiID6orko&G3fjPl*WNtc=-PRaZlnm)?Jukl-EK48ZkKdJER-lI zf3B9e@GR>ri5Mj4f8t(k#JcYZZqKo92F&b;b&YA(Jp|+uv2JQPX5Ge!btT>`>uS@i zdzpgFyx+#mbHU6r_NFu}u}9>yro3MA5!J*j2`nH8WQ;w*c?0Ddf~UMgHKr8@2%TfG zNa9@-6O0Lhhgc$UmBcj~#c~<<BI&e-Wz4RRUtpJCvP%%$#B~xYBvwffA1)tHw3#1a znZ^@E`ku|<Lag#bH_;XreG7$pRYr!n7!JK87b};;EUPF~!b#s-l#AK8yNQ<n*I?Qv z7mzP7?V<~t3f(Xarq8fardp%i@*Jn&u^r>0<PIj%4y+<|C=FxIwrw6lpq;XoPZ=Lh z`jD8MMpA*3Rp#?$9<clPWlbi7v-JNvP81_wkcN}nzTG5}@tvPa(__pb#Boj-_lN7+ zNTUU9uOX7z+<SeJd)pk4*8SJe`hnCcOr+Mr!*Ndd1k;hGPn^QFH+pGuGPYsUuX|*R RrchaQ7F{gf@{Q%2{{X&ENcjK& literal 0 HcmV?d00001 From f7c75e71da012a1a7f50e000d0d1fefc9ad999ad Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <Ibrahim.elseoud@gmail.com> Date: Tue, 25 Apr 2017 18:00:29 +0200 Subject: [PATCH 2/7] Added ability to read batch users or items and fold them in. Also added ability to foldin or truncate new items not in the original svd model when originally trying to foldin new users and vice versa --- recsys/algorithm/baseclass.py | 5 + recsys/algorithm/baseclass.pyc | Bin 11549 -> 11617 bytes recsys/algorithm/factorize.py | 251 +++++++++++++++++++++++++++------ recsys/algorithm/factorize.pyc | Bin 25820 -> 30045 bytes recsys/algorithm/matrix.py | 53 ++++++- recsys/algorithm/matrix.pyc | Bin 7182 -> 8411 bytes recsys/datamodel/data.py | 2 +- recsys/datamodel/data.pyc | Bin 8343 -> 8353 bytes 8 files changed, 260 insertions(+), 51 deletions(-) diff --git a/recsys/algorithm/baseclass.py b/recsys/algorithm/baseclass.py index 681b805..8310a7b 100644 --- a/recsys/algorithm/baseclass.py +++ b/recsys/algorithm/baseclass.py @@ -39,6 +39,11 @@ def __init__(self): #new for update self._updateData=Data() self._singleUpdateMatrix=SparseMatrix() + #new for batch + self._batchDict={} + #new for foldin additional + self._singleAdditionalFoldin=SparseMatrix() + def __len__(self): return len(self.get_data()) diff --git a/recsys/algorithm/baseclass.pyc b/recsys/algorithm/baseclass.pyc index 16b11748948af3db710aff504e77c4d4593df659..50b4b32a88900c7b87a8c0da46433b026d69f6b3 100644 GIT binary patch delta 1145 zcmZvcUue@;6vuxjO_Qcc|D+~O+NP~d`|#`1&2Gb#A}A@fZmj<4tziGiex%8d_Gj~_ zBzFt6tBRs-P|)q{p#^=JYz!+C$-oC;um?d93cl#uMxV!?HW0)I@Asw;QY7%nJ?DJS z{hfQy`Tg$9?aq1Y{%dOd=joe=-WE5_bHshT&oPW|48v+Pq|3NkTl<ZyVN9E37{)0x zz)S8Q60%Vl)<Rllmd2folo6O^r?8l_Q`xK|onj%Kn<(b8PJv&$uZ3Dw+@OH^`N&tN z{#`y7->jdc5cf4W0WZ?6oL#W2VV?4AM%$G`U{hJAP?jar1y6{{yGEb#y7#Wmtk5WL zHLTTk;fuZwO7W)ebyEnLZiQ@Grukv-1z*{g7K{_z=Ko3QDdE&(|7oR<@v*>jig2*0 zuUTi-BmJwDoUKxAkBBRCwz}OECzZI<yiE($)t2+*hgO)t5bw5rO)J%z;2?GB+HpU^ zuOK}NnUbBBwutfVw&dQ!yOn#bnmVve6ytwG(J&kWVlfPS41B_!;h0Jo316if{5bq) zYcJd}AOIZYjrO<_JZQg8KUR~G2@3C5KsLtTM<X6xZ;F45uF~!5V8<j4yiYrz5=`8> z*wb%yuG6m^>H4;I;1og=e6Q;k{mw^Xi{a1qB_KYoE+pSrvNOee-Y#Sx@kiYiRpu|< zSLiVh_Wa-Z&KaTa0D6F9JkopEk2^Fm6{_Wx-uv`Nb>QHri(c@T@yj&9f5*eB)4TN< zp6PF<m%P|No}7l-0x&A3Zh$_)f-)tBcUg>MiET4>LE5FVIM2KNNq<^-1Y{z`L9gXU z7p!Hc;K*lpHwN6J^9Y?&aBA;B5cd97!KZstI|fX_Pt16$E=&&v3KMyLW%f_$%4T3M zD(E3`%2ugZ5wmdOVbZ6tQgjNkY<>@4bZJ9D+FC$<&N>=Je;M&Q6OZULO3J{^H;V zl}llWqCgj*>(QO=gNdovBQ26xH3J_4hX6f=wb6n(1{?=Y0AB!;z!aeGC6AT*WEgl8 f0RC=hft|zwYT@<7kmnNO!~7u8qh64m#A$j9+~?>) delta 1159 zcmZvbOGs2v7{}-2I65z9nz~Monl@UDk2IkKdtlip*hBNAW|m#2@tU`39^HFYN~|b? zc10gi3)?8I40;R%5d^kq8Ci=65(-+is70;X)cK#O5Q$-a-0yYHcmCfwp9b#^7UX^R zy4;&SKPvFqSqBB}?Vb^v?Qn$IY__h5i>B>2X?y-vzem_M<`F+9bLM#%z0E(s0#xa+ zSRLJR+}W^J#JM71<q4RU5m^OK03Ix6Qe*hEQ&kNuW2kBaEfmbjdYxVs7O}0gR5<m= ze&H-(JL!Y-r?B6-F0eN0chAb$m_zQ+C4yyi#k0s1>ho?8p-HcqUnw@}&3^NSnQ#Pr z^K2;l+;^P0(E;BC)QWa>dg!ye%-Dpcj>N23B0ICXhLy_ZUC40@SPp@>p~WnXhv;LV z`S0eX!atF{x@MlS5H%_56gW7tc>~Z0Y@riMnW&gjuCUAWOIh%j!yN*kBHu}`i^Hye z!stSnQ92n6vg_IN!49@#Wjbnh3s_Ddol$j5OPUeLEal~`@I&Jimhu#cFH=wIXJW?f zY_jA4tN!1LVUe|b4O$9KvpG6edM&3I7Ufa;T6%*$po?Xvl#Uezh`0Sy)X?JTWKv6w zJSL-jQXKzF`DONmlIwqjmPvz}AW#PEqN$2)ZfGAt&pbfSD(<tV*|Ex&Rcw)F!<T|l z1aVS%zNq8?Z>6SioMx(u*c+O!iZ=J4dI-Rsi^xIBu@4JV)+BfvKCw+)OIey>@<G~I z-RxF{hYtg-)Z<g#7^a%gQ@ZtX?sc`j<tRdZ0($O6tcJaE;_%7Q=8gsP5IJkYe%YA3 zT=Fp_Xh%&|kL;`;Nt%GXY`Uo$=}CSBPHZMeok{B{%T)CdIbfbaZ~#G^@8F&R^-y<b zSMOmyN)Kx8vG>$hJ9%*pl8S*6K=vc2UjY-(lUD*;fg+#|*bK<aoV%egcLRHYcAyj3 z2Xq5cmju4f3q-^pp#62@%tZ@z^&6)V!{{x)fEm{_=NjT?#!aDp!^BxGd^VoS2I{-n EZ#Zn_vH$=8 diff --git a/recsys/algorithm/factorize.py b/recsys/algorithm/factorize.py index 6d41edb..c5ddb53 100644 --- a/recsys/algorithm/factorize.py +++ b/recsys/algorithm/factorize.py @@ -73,7 +73,6 @@ def __init__(self, filename=None): #Update feature - def __repr__(self): try: s = '\n'.join(('M\':' + str(self._reconstruct_matrix()), \ @@ -360,9 +359,9 @@ def recommend(self, i, n=10, only_unknowns=False, is_row=True): item = self._get_col_reconstructed(i, zeros) return item.top_items(n) - def load_updateDataTuple(self, filename, force=True, sep='\t', format={'value':0, 'row':1, 'col':2}, pickle=False,is_row=True): + def load_updateDataTuple_foldin(self, filename, force=True, sep='\t', format={'value':0, 'row':1, 'col':2}, pickle=False,is_row=True,truncate=False): """ - Loads a dataset file that contains a tuple + Loads a dataset file that contains a SINGLE tuple (a dataset for a single user OR item , has to be either same row or same column depending on is_row aka tuple) See params definition in *datamodel.Data.load()* """ @@ -371,82 +370,239 @@ def load_updateDataTuple(self, filename, force=True, sep='\t', format={'value':0 self._updateData = Data() self._updateData.load(filename, force, sep, format, pickle) - print "reading the new tuple" + + if VERBOSE: + print "reading the new tuple" if(is_row): - nDimensionLabels=self._V.all_labels() - # print nDimensionLabels - self._singleUpdateMatrix.create(self._updateData.get(),col_labels=nDimensionLabels[0]) + nDimensionLabels=self._V.all_labels()[0] #get labels from V matrix to complete the sparse matrix + print type(nDimensionLabels) + print type(nDimensionLabels[0]) + print len(nDimensionLabels) + self._singleUpdateMatrix.create(self._updateData.get(), col_labels=nDimensionLabels, foldin=True,truncate=truncate) else: - nDimensionLabels = self._U.all_labels() - # print nDimensionLabels - self._singleUpdateMatrix.create(self._updateData.get(), row_labels=nDimensionLabels[0]) + nDimensionLabels = self._U.all_labels() #get labels from U matrix to complete the sparse matrix + print nDimensionLabels + self._singleUpdateMatrix.create(self._updateData.get(), row_labels=nDimensionLabels, foldin=True,truncate=truncate) + + if not truncate: + additionalElements=self._singleUpdateMatrix.get_additional_elements() + #If it's trying to foldin a new user who has rated a new item which was not used before, then foldin the item first then foldin that user + print "dimension",len(nDimensionLabels) + print "additional elements:",additionalElements + print "length",len(additionalElements) + if len(additionalElements) !=0: + for item in additionalElements: + if (is_row): #if I am folding in a row then , the additionals added that shouldn't be are the columns to be folded in to the rows + self._singleAdditionalFoldin.create([(0,nDimensionLabels[0],item)], row_labels=self._U.all_labels()[0]) + else: + self._singleAdditionalFoldin.create([(0,item,nDimensionLabels[0])], col_labels=self._V.all_labels()[0]) + self._update(update_matrix=self._singleAdditionalFoldin,is_row=not is_row) # #update the data matrix - print "updating the sparse matrix" + if VERBOSE: + print "updating the sparse matrix" # print "matrix before update:",self._matrix.get().shape - self._matrix.update(self._singleUpdateMatrix) # updating the data matrix for the zeroes , also for saving the data matrix if needed + if self._matrix.get(): #if matrix not there due to load ignore it + self._matrix.update(self._singleUpdateMatrix) # updating the data matrix for the zeroes , also for saving the data matrix if needed # print "matrix after update:",self._matrix.get().shape + self._update(is_row=is_row) + + def _construct_batch_dictionary(self,data,is_row=True): + ''' + + :param data: Data() + :param is_row: Boolean + :return: constructs a dictionary with the row or col as the keys (depending on which is being added) with values as the tuples + in self._batchDict + ''' + # self._values = map(itemgetter(0), data) + # self._rows = map(itemgetter(1), data) + # self._cols = map(itemgetter(2), data) + key_idx=1 #key index default is the row + if not is_row: + key_idx=2 + + #collecting the significant col or row tuples at one place to fold them in at once + + for item in data: #data is a list of tuples so item is 1 tuple + try: + self._batchDict[item[key_idx]].append(item) + except KeyError: + self._batchDict[item[key_idx]] = [] + self._batchDict[item[key_idx]].append(item) - def update_sparse_matrix_data(self,squishFactor=10): + #batch loaded , now need to fold them in one by one + print "Batch loaded successfully" + + + def load_updateDataBatch_foldin(self, filename, force=True, sep='\t', format={'value': 0, 'row': 1, 'col': 2}, + pickle=False, is_row=True,truncate=False): + """ + Dont forget future work in presentation , remove old and insert new + Loads a dataset file that contains Multiple tuples + + truncate:boolean-> sometimes new users rate new items not in the original SVD matrix so would you like new items to be truncated or folded in ? default is foldin + is_row: boolean -> are you trying to foldin a row or a column ? yes->foldin row , no->foldin column + See params definition in *datamodel.Data.load()* + + """ + # call update here until it finishes + # nDimension + if force: + self._updateData = Data() + + self._updateData.load(filename, force, sep, format, pickle) #load array of tuples + print "Reading the new batch" + + self._construct_batch_dictionary(self._updateData.get(),is_row) + + print "Folding in batch entries" + nDimensionLabels=None + if (is_row): + nDimensionLabels = self._V.all_labels()[0] # get labels from V matrix to complete the sparse matrix + # print nDimensionLabels + else: + nDimensionLabels = self._U.all_labels()[0] # get labels from U matrix to complete the sparse matrix + # print nDimensionLabels + length_of_dict=len(self._batchDict) + i=0 + isbatch=True + for key_idx in self._batchDict: #data in batchDict in form {key:[(tuple)]} + print "user:",key_idx + i += 1 + if (is_row): + self._singleUpdateMatrix.create(self._batchDict[key_idx], col_labels=nDimensionLabels,foldin=True,truncate=truncate) + + else: + self._singleUpdateMatrix.create(self._batchDict[key_idx], row_labels=nDimensionLabels,foldin=True,truncate=truncate) + + # if(i==length_of_dict): + # isbatch=False + + + # If it's trying to foldin a new user who has rated a new item which was not used before, then foldin the item first then foldin that user + if not truncate: + additionalElements = self._singleUpdateMatrix.get_additional_elements() + print "dimension", len(nDimensionLabels) + print "additional elements:", additionalElements + print "length", len(additionalElements) + if len(additionalElements) != 0: + for item in additionalElements: + if (is_row): # if I am folding in a row then , the additionals added that shouldn't be are the columns to be folded in to the rows + self._singleAdditionalFoldin.create([(0, nDimensionLabels[0], item)], + row_labels=self._U.all_labels()[0]) + else: + self._singleAdditionalFoldin.create([(0, item, nDimensionLabels[0])], + col_labels=self._V.all_labels()[0]) + self._update(update_matrix=self._singleAdditionalFoldin, is_row=not is_row) + + + # #update the data matrix + print "updating the sparse matrix" + # print "matrix before update:",self._matrix.get().shape + if self._matrix.get(): #if matrix not there due to load ignore it + self._matrix.update( + self._singleUpdateMatrix,is_batch=isbatch) # updating the data matrix for the zeroes , also for saving the data matrix if needed + # print "matrix after update:",self._matrix.get().shape + self._update(is_row=is_row,is_batch=isbatch) #Do foldin on the singleUpdateMatrix tuple + + self.update_sparse_matrix_data(is_batch=True) + + + def update_sparse_matrix_data(self,squishFactor=10,is_batch=False): #update the data matrix # print "matrix before update:",self._matrix.get().shape - print "commiting the sparse data matrix by removing empty rows and columns divisi created" - self._matrix.squish(squishFactor) # updating the data matrix for the zeroes ,#NOTE: Intensive so do at end - # print "matrix after update:",self._matrix.get().shape + if is_batch: + if self._matrix.get(): + if VERBOSE: + print "updating sparse index" + self._matrix.index_sparseMatrix() + if VERBOSE: + print "before updating, M=", self._matrix_reconstructed.shape + # Sim. matrix = U \Sigma^2 U^T + self._reconstruct_similarity(post_normalize=False, force=True) + # M' = U S V^t + self._reconstruct_matrix(shifts=self._shifts, force=True) + if VERBOSE: + print "done updating, M=", self._matrix_reconstructed.shape + + if self._matrix.get(): #if loaded model there is no matrix + if VERBOSE: + print "commiting the sparse data matrix by removing empty rows and columns divisi created" + self._matrix.squish(squishFactor) # updating the data matrix for the zeroes ,#NOTE: Intensive so do at end + # print "matrix after update:",self._matrix.get().shape - def update(self,is_row=True): #update(tuple:denseVector tuple,isRow=True,, - print "type of S",type(self._S) - print "type of U",type(self._U) - print "type of V",type(self._V) - print "type of data",type(self._data) - print "type of matrix",type(self._matrix) - print "type of matrix reconstructed",type(self._matrix_reconstructed) - print "type of matrix similarity",type(self._matrix_similarity) - print "dimensions of S",self._S.shape - print "dimensions of U",self._U.shape - print "dimensions of V",self._V.shape + def _update(self,update_matrix=None,is_row=True,is_batch=False): #update(tuple:denseVector tuple,isRow=True,, + if VERBOSE: + print "type of S",type(self._S) + print "type of U",type(self._U) + print "type of V",type(self._V) + print "type of data",type(self._data) + print "type of matrix",type(self._matrix) + print "type of matrix reconstructed",type(self._matrix_reconstructed) + print "type of matrix similarity",type(self._matrix_similarity) + print "dimensions of S",self._S.shape + print "dimensions of U",self._U.shape + print "dimensions of V",self._V.shape invS=np.zeros((self._S.shape[0], self._S.shape[0])) for i in range(self._S.shape[0]): + # invS[i, i] = self._S[i] # creating diagonal matrix invS[i, i] = self._S[i]**-1 # creating diagonal matrix and inverting using special property of diagonal matrix + # invS=inv(invS) inverting with numpy #if new is row -> V*S^-1 if is_row: prodM=self._V.dot(invS) - print "dimension of VxS^-1=", prodM.shape + if VERBOSE: + print "dimension of VxS^-1=", prodM.shape else: #if new is col -> U*S^-1 prodM = self._U.dot(invS) - print "dimension of UxS^-1=", prodM.shape + if VERBOSE: + print "dimension of UxS^-1=", prodM.shape + + if update_matrix: + updateTupleMatrix=update_matrix.get() + else: + updateTupleMatrix = self._singleUpdateMatrix.get() - updateTupleMatrix=self._singleUpdateMatrix.get() if not is_row: updateTupleMatrix=updateTupleMatrix.transpose() #transpose - print "dimensions of user",updateTupleMatrix.shape + if VERBOSE: + print "dimensions of user",updateTupleMatrix.shape res=updateTupleMatrix.dot(prodM) - print "type of res=", type(res) - print "dimension of resultant is", res.shape + if VERBOSE: + print "type of res=", type(res) + print "dimension of resultant is", res.shape if is_row: #use new value can now be concatinated with U - print "U before adding", self._U.shape + if VERBOSE: + print "U before adding", self._U.shape self._U=self._U.concatenate(res) - print "U after adding", self._U.shape + if VERBOSE: + print "U after adding", self._U.shape else: - print "V before adding", self._V.shape + if VERBOSE: + print "V before adding", self._V.shape self._V = self._V.concatenate(res) - print "V after adding", self._V.shape - - print "before updating, M=",self._matrix_reconstructed.shape - # Sim. matrix = U \Sigma^2 U^T - self._reconstruct_similarity(post_normalize=False, force=True) - # M' = U S V^t - self._reconstruct_matrix(shifts=self._shifts, force=True) + if VERBOSE: + print "V after adding", self._V.shape - print "done updating, M=",self._matrix_reconstructed.shape + #TODO: contemplating removing this segment and just reconstruct in the updating spare matrix function + if not is_batch: #will reconstruct all at end with batch using another function + if VERBOSE: + print "before updating, M=",self._matrix_reconstructed.shape + # Sim. matrix = U \Sigma^2 U^T + self._reconstruct_similarity(post_normalize=False, force=True) + # M' = U S V^t + self._reconstruct_matrix(shifts=self._shifts, force=True) + if VERBOSE: + print "done updating, M=",self._matrix_reconstructed.shape @@ -475,6 +631,13 @@ def update(self,is_row=True): #update(tuple:denseVector tuple,isRow=True,, # myFile.write(str(invS[i,i])) # myFile.write("\n") + def printMovies(self): + myFile=open("movieIDs.dat",'w') + myFile.truncate() + + movies=self._matrix_reconstructed.get_col_labels() + for movie in movies : + myFile.write(str(movie)+",") def centroid(self, ids, is_row=True): points = [] diff --git a/recsys/algorithm/factorize.pyc b/recsys/algorithm/factorize.pyc index 8ddc6846c4d74c0e30b323e23e17fcfaff1d0441..79f7199fc03ca842f12de55634fa87558ea13eb6 100644 GIT binary patch delta 5927 zcmZ`-eQX@X6`$F?JNwROpFd(JmpJi;Byff#*a?u}0M3UKCn7f0CXmRG9_!n+ea`vL z-d)EwwR;6S2toMVhL-ONs02kq(5qUC3Z*Trgh1ktN);`E(5PReX_FTEK`Yw+-rGBW z5SsYj&AfT@X6DU%@Au|TydmE^Ez8u8DkCp^_tO{4o>ptWD21?u=#jz%RfO5(y`pa3 z)g-KPVTQzvB6=diECZkZtP19%T`p!qq9@Ayu$Ymer;^(dZddv8-j=}D#h7E(vvdtl zZeWR8?}osdgEY37$LfR`CH3YKUULzzR?eKbm{CPfy)c)G8SJrvwJc*Ti-p-J%;n5k z!D35<zAvE_UJZN_kb4U+g)b_T*B3shK2RnfFKk|NtCS}SFUNOFd8(i;+Z2|k3tLui zk@Awl*cvHiOJU+dOO=-wPBzyC<+a{N>#wUT7eZ9vL*F~_aheqG`b#(FT~*la!jz(O zcUFkWkg%T<lR^vz#9&ZN3t=iEK2<K<keDnN(<J6MiSKvIW=CjVxfl$y0x1R~tU#r4 zGay`8B`7A#phcD0b{2}}i_2gMNOkW1d=<R-XH`Ix^NuV|sHCN)!gR%*?%a-Wznzm| z=ruuzA1<Du)Xd`?hhc(u<oLrh+HvS!F%@Au+{(JE=sXajGvLUR5z(iLez+O-gyTl% znQ0oP&u(P)>@2AACl+VH7z>P>Co4)`DJD!vV-#LjK|6>}3q_8!%L60tNZbwG)bAwR z%fBfIRu@fdS12?O-5OTF7e!Nu3$?6mT1aVDihYSHy8SaC5lz#))h+S7<_;LHmda&a zBb{|LL)*8fyL0bO%^e-iSX#o+Ov5!C%hmdFHb$Lvwjcaa$FjBSbS>>#L)uzxz;HA- zr}bKzm39X(<QPMiX6ME<OyYJbml++(YNj=8WliYNa#=0yB<)Jb7>5mixn}R`mP7e8 zwkO;O2qq;nMz57|;CDjw<uaJ+k{7%7Xf|cI7K>q>l2{mHW_rlVI_X^2S%jWpnrRmu zBcoXv3nQ-6#@aGgw%;9aD<LyFj6+(<A;Y!P<3ar>Sl%lwD`l;BvgLw6HM$S9)>j7b z?M@6^`W6~r)VeCU0S$dIv5E9sZgOt5WN9@XOZs-(HoKW!{#nW}FX=C@_md(zr{7Oh zcwMcREBY}n-?}93)?#2z`_AG~^xM6a2h(zy_n+3q`J`{4L8L?HDu|1+kc=sfN=TN= zkgSsxGKSB3H70}d5^!TmOoqX$m6aI3LfI%+tB?h^K}o5YAH;_?v;!aK4iKb@DR{9W z9HcUZ+Hkb-elaN#Jo%)U3W$kD_B{AsKuoG49>;djM7n@4H;RCZn2QMe7I>t@iOEZ6 zWv^P$n>P~^58`vcR4JSTaAew=a~6agng2yDF16Z*4ci!E$8FO(;J%6GSt)<aOZLR` zZMNl(+SxX8eg_Vfa>@P8bc)@@p3uhN%LtEIM}|9U<mliWwk8}cvA~<h2GXek_%xz` z{2O;=na#eABSvP_f^vT@N0t-Gm$sYEYL1oZTc7MT+|)n^)}P&N8doXO3Y|3$vYiC9 zOwNF6%^6LlEXU~^&15FJ6Lei140Bp^@&S&PYpsc$ww<%tYPtrtzFpLVlBXjx3W4(^ z)8@GEPG49CPIA^NzQ*L7VICm|qu3G^AvjJrP>H182q&tMnwp~UjNwC}$c~dGu(>ji zi}n*Bzybb%OAm=@MM(G&?zBb_3IKtQuuFu*Ag}@s2M@wPwvM^Lm-$@R2OA&_+!Bde zrR)u)nW_-u1x~9GxaBi)3J@R20}LpAB8dZuHWl;yC#)BVypkKp!BG*X^p3<`#;Ao| z6;Z4Cb7irI^JNL1syQ!1brl_CDv#PC0vuv_22Z5s&dEwTJoZI=;22|Usfhy-JXI?M zPnAAARk4x2Ujz+o8$z@J*lDt=1W!-^YtOgVw9L=qQ;bea)1b?rxd7GXo4y23)dJ{2 zIsquW10SBo5Yp8OQh*sF%VNByc(?XeK0Ug%MXTk_YB5=X6Jj5J*h4AnDh?3dJueQB z78Fv|WPFCL8bmuKKPQMdn2ihyo(_N(!4eX$);Ee+wzRRFeON<44cnH3RBuoy1D|Yb zXj$%vr6K8RM%IJ`@XOV*))*`FR<<;L76`jWGj5t7uo$ijRxZI$TW>Cxv5f4xYcwY} zWVt{dhqN+2I+_hs;V!`>268U#jq*p%PWPt?llI-vp)u~{$DsoIAH^0Ya-&)%eR%#{ zA6-l9nuM#A?U4tdu^qVDXJEsWetZyEuw4S$+*q4d+^vS~8ZZ_u<JuFHt=wEkaiV6F zaN9s)WYBKcCM;*&HNIjVU5m5KGJGlDknJtoIIk^O9$<l7V;PM5bb7K7Cp%{eMs_h2 z_R~R_so~=6wB>Le;83EcYNL&Fp?)W+x{Js$B6kzH2V@TGS9}KS^?OO?J|dLv0PIb9 z&Wf-Kuh*TRT&9zc>Gu<PfXGus28c8eq4cYJL<s7+1kk@htqnwu6FEfWK_Xux@^vDF z>H0$;E-5J18BQJm<&^4;Bo$Z*%9ws;UP-M>i9Af?5h9Ngq22qZehe+Q3dFBrlDR(4 z&iYQ`Q#wyOd@}t>wDre{+zyh!hl#iD;GsMvd4iH+`9V0jo$#etMSPEv{9c+-$yJ05 zDsd%<bRUupY8`3>YE`3th~Rsz+Dx?u{?FADO>(i)Bp1omfgslzb;?q|s!*XbD1Zk$ z&~V-WK|x_3=RASnmk4`dZxkpL>{WA*6O7<br9m!;F?uc4Af1#j_`xz;8iy#m!4C9j zKsk)WLNWjdyg<+Z0AL{RkI*W|si2>)XXH55H6)8ae?LFv{d}9>82niB2T@&P9JLMP z@%Y@AAoS~Ihv&<t*mC%`;ohb-?$lwt*Fw_rp5L_5OK&*1ISP*NC+uxhZE~^4@=1Sk z7gwe_U71S(;S3nVp0T+p!S$juGMaV<xCCGqa!IAn+-0QPoUK3Y-QS(ge-mtpB<id{ zL!i{7ODxAFhGebMpv3WA&lg)RQ&j8{g41ezPv<bv#l?Jwuf#=B2MZ`ZV$n4eIk+Ig zFkC>1!?hn(Z9w3n5FVE-6rAOt6krbSp(sP3ON!`C=Py=#g7UMD*T$7X0ED)p(hZ{v zjHNC-g1WdCR7HchCL!|JE_p!(pt3ZdI(>W!l&<4f3Yc!e81~R{Xr-9)JqItrAj*TN zsH5T*yr973t42NsY{8HAu%BUNU?rb=D#Rumrngt;WFE*$dm0&**^77??D>4rDu@;( z$qA*fk4n&D8Br#(F`f$)_X`WWW>JqnD^YXc6^QVdf{cGxuSx=D8lZLaqz-K?$>ye) zJudVw5@5?VE*ZE6bNDImM-)!hu8?F*F>5h2XKYgF_we@Q+2|$_dk~&c%(?}_Bl#I6 zyOYNbDCQ!6Z0?#!5qk3#AxeEk_2&tzc*XLbo>a^Rf3$eY;+|^hM;RA|#1i2y@^ipO ztbk`rocG)?Z-iDkqu{3*K3h@G$QdeZ3i66y%PLO!6qx!~LA<k@Rt5EMQLAxtQ;4JX zxVLTdg%|73kqjfkYHBS7Nl+B|nSB<65Rb<;KanSdh*LZwb09Jih4cR-67e)sn&on( zDL`Wsr9@-?(~T(gN>mAfQ!DGit4D16ty;8bo*&&QdQMPe(`N@VuyFQ+;QV;lA&60q zPa)ReY@GDioe?vfbf*+CzJ^?_<Ir+`1acXVY=lQ1q67iI7+-SaQoPbA5z-Rf4rQXD z+>x}kr^8tfpwoBLdbEwTi5_}d>X#8YMuf9l6<YL=o_qT1duebL2<!AS98DdX*p<#$ zoB??Whf^ary>lD-E}&n$hHd;{&_x??ZXruYz!R`o1lFyTg|A$Bn^bO73V|&nfzaz1 z(qHqw)c&UYq1U<fvWnM;@gpKX@owMR8Tv6bU-y2#bt3d8ns+OnzHNt$dk=2gFQ4=N zxNZ6BCrIKcBF_+^gzMJ<KcE&b@TS+)oAvJ69$vc^yihnKqf5i#@Sf;3u~4jh*|umf z8jLm6Hnp@y8&}BMJ+URVM!3B2()PC%dBVG6=Y9CuFTU$?Dc|?5>s%gsj})Ku?9Myo zhlNi&Yl9Wc`7N<O@RoM%gxt+t%j79<vg@MINn-!Td$#LV`H6Q)_tj0mB*w3Zyi0^j z9j?|`!3J+>uM<3hkzW-4+x?abUBHnL3AC%;mVIx@GhX@r8Kq10?%97+%~|mII{f#p ze$M;%{zsJ?RPUkd&o2Et>F3b+6IuyeMlmWr#xEF0La0xjn^bSbfe(F2j-)@IFL{e% zsfEW5o{}M24NDyDX|4H~nnyv{vrPSO^rMK^a${7<s)cKAToY`XGs-!VCB-@`pv|zM zmU~a!ylVUV)SV*oWg@3QP$2-aNByrfeu$%9TK_$b|A7b@mDx^uQ2)>i7>_B_s`r%9 zvGM;}_pdaMjlpL52X+5R<X>d*)u+*H#X=6{oDkJtRpD?f6b(nivHC<a3BK#S*gGhH c<~63?md|@<QtLu=P5NoC$$VLTq2b2=0p^GV2mk;8 delta 2297 zcmah~Yitx%6h8OP&TMzP+i7>{LSMGn0)>d$2Liz=NNGz@fdyMqEM@6T+ud%rv@=yO zTME*KU<eX#OpHM24-*lIhRiPwhR9n$K;nZCkcaw1_@N1ji9bv{=kB!BL}Rx1%sJnA z-gD2ry>*}6{EB&%he3bW;LpdrA1I+9126$v85l@87{%fXWkEwRm|0+GFv6kL4~7?U z`ZKd7j(ir3XwVvvxDQ4cv<4;bmwZmfUM%t~UTWmShzhNFV4!tGszWl$RLO@WKTYye zz$gHtP$ESzqCjgtMAuZ7IJ)|$hrQ(Z{Ik5Q*{RBF@v=kC?rA$1yX9mTHZk^-v$kly zk3DeqR4iib3FrI_##oheVWz3DXPl?!gjM#UsDEl@*b4wzxHMc#aakuo)Gb&ztieD( zqyZ8NBs{PaV8|;)6%v|c7$m%e8ywQu4h0H!Vv1C?RA&93lV`G%p+aL5<?=zopUIL7 zI~g!+_?k+p(Ap?2SCxr3s@uwKKgu0_$@XMylbN*QD1pQV`d)W2vA$+H%N9d5kB1A! zwXx*5hd5JH9+`@Gw2PFq@xjeT%r+~tL}Tqdv7<I$uyvioQ&5rYiy7@1ZGFs+tsUH) zG<$GDtOzQ>t1+DyGnLVIrlRw5MdckFHRDTmlxi@TyG7h3gEY%*4{&>A`mz;8+R@kB z8@J=Bt}?sZEEA)PmqkeVs7$MWFm82Ma`EJn0xD75s4E%XMdf(V4iuHi<kT<~mrUM= zAt<>G?x|s48=LUZ#t}}XAfiklii>zA5F7w$gXiSNQC&nEo`5tL@=hQ)AJQ!g8tHU# zVvuVxd>N6uqCX?zoRQX^m;q^SWJAG(FA1Hp-XRWK7J{7(>EZFfq{MXfZPQ^$arc_W zsl;0rPUDOsrT}_B+y4Ms@BjwpGk3fLfO>Qt@_-S<Vmz5jQjf_fSmtDdoJR)a9vp^5 zj=UZ!ovO8!rSXR7E|S9D>*#<G?Tx+|OLlWe10y%nsqx0;p{3?pVto&o5kdr$xSev@ zTtF)<7sYlC!$$ZYMyVPwreqMeqvVF{WH$xy5t5-@8+?bPIG!F|@O_#Jb_k4VV25!( zT((QTNE9*ORfI`{Pt;_0=XU*F7Njd;(_q?LH=E*eL%H9kskFLdDE`q<s=bX;v|AK4 z&YU4N4Y8zUMym;}AX11#-u`GAiKC*Y@z5*#kljlo--HaX(BvQ28UB>ntb~;+8ftVN zVgW?)ezE}RP|{G&XCZ~C&UGH)1>Cv6{7uGhGbgp8-=iI+Y=^}!D=)GS#j#bj*@p@E zh{(s{_f?JB5n_*uy471X2if~f9ACYZMc#?7tr;U#Cy_xSTZn8YvWv*OMBXDp+lb0U zju;zA%?+bmiOZs$Yd(zyihMrb^1yStre_r`38(>8kAzC+76-h6)y~2-S2#N@e9imV z^UnLt^B6lTuC|nDXDI49@v!AB*5GVg7gDn&@-@Nd#s0QsXm_`*h+PnWwawDb5qwF^ z*|38(i=!LrO1~uFB#~1@=m4YnL}Y+!v46l)PorGpY;V7$V60DTyS>ul02jwQF0pUL zZexTW;X*fG&HDi@<?H%dEIDXe(d!~@9^eiag`K|@{zzfvQGbV=d;~M6d=rDl#h$J_ z{uvjCx_-@Aj*<EI|85!7@V|j|k~@#Z?=g+CVk)htD(@z-qeyI=7cio?Way16Jbufx z+k4Ge>LPbe^vqC8A9d+E`B0*$3`E7r&XtI={_+>jk~l<U8<7i0Y<i+M4Q4)ZTai*c zdWG~?iBOj%Y{e7NZ^UT-L4Jpeih=r?|7`aS@{`SxZMse39U}MW2r8~2yBE`1xyWci v9PS*SPuBvzfKSh_oK1$OM75P*pNNCj6}DgW+H*BJx9Bx7W`C{(BkTVL4nyK= diff --git a/recsys/algorithm/matrix.py b/recsys/algorithm/matrix.py index 85da209..b4fc51e 100644 --- a/recsys/algorithm/matrix.py +++ b/recsys/algorithm/matrix.py @@ -76,27 +76,68 @@ def __init__(self): self._values=None self._rows=None self._cols=None + self._additional_elements=[] #if no additional then len will equal 0 -#`nrows` and `ncols` specify the shape the resulting + + def get_rows(self): #can use to get rated items and remove from recommendation + return self._rows + + def get_cols(self): #can use to get rated items and remove from recommendation + return self._cols + + def get_additional_elements(self): # can use to get rated items and remove from recommendation + return self._additional_elements + + # def create_blank(self,element_id,row_labels=None, col_labels=None): + # if col_labels: + # + # self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols,row_labels, col_labels) + + +#row_labels specifies the row labels the complete matrix should have incase the inputted file doesn't include all indicies and it was saved in previous matrix (for update) +#same explination for col_labels but for columns #matrix should have, in case it is larger than the largest index. - def create(self, data,row_labels=None, col_labels=None): + def create(self, data,row_labels=None, col_labels=None, foldin=False,truncate=False): #is_row is what I'm originally folding in self._values = map(itemgetter(0), data) self._rows = map(itemgetter(1), data) self._cols = map(itemgetter(2), data) + + if foldin: #new to make sure not folding in user and item at same time + #idea: create matrix normally but keep track of the columns (items) or rows to be folded in before doing update + if col_labels: #if col_labels defined then I'm folding in a row + self._additional_elements = [x for x in self._cols if x not in col_labels] + else: #else I am folding in a column + self._additional_elements = [x for x in self._rows if x not in row_labels] + if truncate: + for item in self._additional_elements: + if col_labels: + index_remove = self._cols.index(item) + else: + index_remove = self._rows.index(item) + del self._values[index_remove] + del self._rows[index_remove] + del self._cols[index_remove] + + self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols,row_labels, col_labels) - def update(self, matrix): + + + def update(self, matrix,is_batch=False): #isbatch is for creating the final sparse matrix ,since you will want to collect all then construct final matrix at end +#To update the stored data matrix with the new values and create a new divisi spare matrix with it to retain the zeroes self._values.extend(matrix._values) self._rows.extend(matrix._rows) self._cols.extend(matrix._cols) - self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols) + if not is_batch: + self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols) - def squish(self,squishFactor): + def squish(self,squishFactor): #remove additional empty fields created by divisiSparseMatrix self._matrix=self._matrix.squish(squishFactor) - + def index_sparseMatrix(self): + self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols) def empty(self): return not self._matrix or not self._matrix.values() diff --git a/recsys/algorithm/matrix.pyc b/recsys/algorithm/matrix.pyc index 0c5cba451931863960257260cb5c4e1e2c7a9d74..f4df053d8a20bc30468603f38761637e803476ba 100644 GIT binary patch delta 1216 zcmZWo&1(}u6o0dyNlc~5CT(mLdQcl%TeTGwj7VFteqgOztWvOH&8~4{x@mW(8ZkxC z9t5SRqu@Uvcoh$VBK7Fan-@{^;#G<lFM{t)Kc&pfZ-4vVoA=(lH?wf~%Ydo=Zc4m+ z^Y@kcy*Z%+z#y*ZnpkSK#4t;pa#ffIn9#wE)AUT5t_JfOOvEJDVV=PRY7J?b0N0dc z3%D^!l3<VJ6XHNB%Z`ZQ)PZanpaD=sKLBHSh5<+NF)^Pi<ah>uijM30+^+>rwdhs7 zDKFq+F?F*kfjVFoi_;Jlqf6^Xf%uTBZsjp#Wseub+Rexoj?u5}-J~nks?i0h>siqq z;jJtCHiwKZp(;`kfRzlyI4=4Q;fjWs7*C(R)6gKyv4#raU06O<MT6s)b1-K>1E&ht zO$aB6Pbx5j1dpdf(7kG{ZnRdU^}|(bB+|N}Q;>R*ek(d|I1h6s`3}dwNtp%}jAg<- z26Hh8(o(4YC#0<kG-7a+n7YO^$!6Ze#hDxn0JsU2n0S%yID-wP?r3=05g!43YLrg} z$x|n|iZJC&+a$5O2&f%4)!n87bGl7_;CgkztiDkypH20%w-B@uWDz*YuGXrqAIL89 za5gA8+~X9k&MCgny(yj~Lf(p@H&wggiBGw50aJvi0|8i$hGHlN>s7j$!J2Vpl{8D> z$fzaCmKOPw9W0B_R$)&=fv5x+1q(}+kvEenC98A~3xzE(AOO*q>F(;s&Gtb8I$NIn zEg#83CbRdW;uVLNu8E>q5??b3>keund2<yD@2-XUi9eZ-`cahM$IEDqt!1=}oM2og zqp#*bnP{+*2|mO1CnJlwp3;UEh)2e3YZ~1x7c81~!pJ-8@X)V6z?j>`kL=5H3zXUN z{T|6T;g7wWpp(EP=q1=ku%CePls!G+R3p!(e%J*9I*NTvyluJFLNN^%kHu9S$!vQv zPD~86K8+8O75T*nJHwuc(cEn|B$jl~kglG+KC~^(*mH5ZZKr7ukZ}@SPV2PqvG~>2 V!}`UJ_DMD<7F#OfXL~0*`41yl<5vIx delta 522 zcmX9)Jxc>Y5S_hb?{cSNk~1I1=tj|a2r3CEf;K8DS_obm5fMW!K_!srDG0GltIhaF z{HTJZh5iOXEG)!MENz^*u=^hG-p;(8y|?9?1tav^XZ`Je-WeZJD+B;51b-2KqV}o; zkG6oe276)fG-w0tkrN7p4qiAA2Dl|hr?!e9VsOqX!7t)fb`Y;)>0S?@<nY<HV_+w? zgr{~LTX8EFArFKtOj?yNX$GeS0Y2dCz?M9haH8W^A}0(qlS3kc6G?@oWHG_jE|<GK zOc|yG?x>p6%aY)CZ%7MX6^$GQ_XRf`;ALtFJE>88PfcZJ$aZI$B$30F-E<p&WNnBq zx@dak^Xe|CZn9?6^tOig=@(ff_YhAr&xI<Vb32G_hbM6JOp{CoQ!sciV}U=5xS^UU zQm{$zEh1f*k$&Bbm=f*mtv*MVYoHov=t06Ujt#fOJik?PPW{0gi3`!dtQxoIhXJgY f>wXp2)S2YoBYaZRVg|p}L6GGDKM5amd7=IR*r`^h diff --git a/recsys/datamodel/data.py b/recsys/datamodel/data.py index 21de618..f2f3d94 100644 --- a/recsys/datamodel/data.py +++ b/recsys/datamodel/data.py @@ -121,7 +121,7 @@ def load(self, path, force=True, sep='\t', format=None, pickle=False): self._load_pickle(path) else: i = 0 - for line in codecs.open(path, 'r', 'utf8'): + for line in codecs.open(path, 'r', 'ISO-8859-1'): #was utf8 changed it to 'ISO-8859-1' data = line.strip('\r\n').split(sep) value = None if not data: diff --git a/recsys/datamodel/data.pyc b/recsys/datamodel/data.pyc index 57a97e63b45c10a3996f3fd0a234824a80e45c99..176bf003ceacb312e03caca2d384a9ec88644cf4 100644 GIT binary patch delta 269 zcmbR4xX_WE`7<xqvXkF8vKx!?7jrQ%Fn9+0>snZtTIw2Zb{4(Jtl|I^3vvPx#vsB3 zM6d!04JHta4Mf;XR+LX+G}=5@GLXr=gas&6T9Rf_%m`#~f=Mnv4PKB!uois~%K$_e z0*MkHAQ4}jSe6=JkeQsFlM1zFvZ0I(n+-^n!{%a{7Dhd1Alpv^WO$GRkN{hgQkq); sb`02A=q{SvDc39jcDWHqmlud|oy@C{F}X$FlpE{}u$LSrACbQd0QkEy3jhEB delta 211 zcmZ4JINgz*`7<xqubYoIvKxzXmas4|FqD?0S#0(cy~wQM2;>Dhg9sB4VG1HxfrJJV zh{XmXY$tDEO=b!*-n>{ckcl-&3n(`Eue1cW0Z7siL>Nt0kP&5r7%<sd#)i!nB<i@i zR;Gnf&jrZ#(*W5T1hNcle@ba?0oWR_h3Ixoo+Q`826mJ2WEuJ85R1U3J5D|)e;EJ? CoGHZs From 9c1d42d6b9d40b45212c735509d08cdc82b86995 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <Ibrahim.elseoud@gmail.com> Date: Thu, 8 Jun 2017 14:34:36 +0200 Subject: [PATCH 3/7] Cleaned up code for commiting to github, on a much much earlier date, I added the ability to recommend items unrated by the user for folded-in users even if we loaded the SVD model instead of built it from scratch which wasn't previously allowed under original implementation --- AUTHORS | 1 + CHANGELOG | 5 + recsys/algorithm/factorize.py | 406 +++++++++++++++++++++------------ recsys/algorithm/factorize.pyc | Bin 30045 -> 32828 bytes recsys/algorithm/matrix.py | 10 +- recsys/datamodel/data.py | 178 ++++++++++++++- recsys/datamodel/data.pyc | Bin 8353 -> 12984 bytes 7 files changed, 437 insertions(+), 163 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8003cab..eb97b49 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ Oscar Celma (ocelma __at__ gmail __dot__ com), http://ocelma.net +Ibrahim Abou Elseoud (Ibrahim__dot__Elseoud__at__ gmail __dot__ com), for updating SVD model part diff --git a/CHANGELOG b/CHANGELOG index f15b79f..ad6bba0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,3 +28,8 @@ See: https://github.com/ocelma/python-recsys/commits/master 2011-10-08 * Added the whole project at github + +2017-06-08 + * Added updating the SVD model incrementally by folding-in + * Added a capability to split the dataset for train,test,foldin to facilitate testing the Fold-in implementation + * Added update to github diff --git a/recsys/algorithm/factorize.py b/recsys/algorithm/factorize.py index c5ddb53..75e893f 100644 --- a/recsys/algorithm/factorize.py +++ b/recsys/algorithm/factorize.py @@ -26,20 +26,24 @@ from divisi2 import DenseVector from divisi2 import DenseMatrix from divisi2.ordered_set import OrderedSet - + from recsys.algorithm.baseclass import Algorithm from recsys.algorithm.matrix import SimilarityMatrix from recsys.algorithm import VERBOSE from numpy.linalg import inv #for update import numpy as np +from divisi2.sparse import SparseMatrix as divisiSparseMatrix +from divisi2.sparse import SparseVector as divisiSparseVector +from divisi2.dense import DenseVector as divisiDenseVector + from recsys.datamodel.data import Data TMPDIR = '/tmp' class SVD(Algorithm): """ - Inherits from base class Algorithm. + Inherits from base class Algorithm. It computes SVD (Singular Value Decomposition) on a matrix *M* It also provides recommendations and predictions using the reconstructed matrix *M'* @@ -72,6 +76,8 @@ def __init__(self, filename=None): self._file_col_ids = None #Update feature + self._foldinZeroes={} + self.inv_S=None #since it doesn't get updated so redundent to calculate each time def __repr__(self): try: @@ -124,7 +130,7 @@ def load_model(self, filename): idx = [ int(idx.strip()) for idx in zip.read('.row_ids').split('\n') if idx] except: idx = [ idx.strip() for idx in zip.read('.row_ids').split('\n') if idx] - #self._U = DenseMatrix(vectors) + #self._U = DenseMatrix(vectors) self._U = DenseMatrix(vectors, OrderedSet(idx), None) try: self._V = loads(zip.read('.V')) @@ -140,7 +146,7 @@ def load_model(self, filename): idx = [ int(idx.strip()) for idx in zip.read('.col_ids').split('\n') if idx] except: idx = [ idx.strip() for idx in zip.read('.col_ids').split('\n') if idx] - #self._V = DenseMatrix(vectors) + #self._V = DenseMatrix(vectors) self._V = DenseMatrix(vectors, OrderedSet(idx), None) self._S = loads(zip.read('.S')) @@ -148,7 +154,7 @@ def load_model(self, filename): # Shifts for Mean Centerer Matrix self._shifts = None if '.shifts.row' in zip.namelist(): - self._shifts = [loads(zip.read('.shifts.row')), + self._shifts = [loads(zip.read('.shifts.row')), loads(zip.read('.shifts.col')), loads(zip.read('.shifts.total')) ] @@ -198,7 +204,7 @@ def save_model(self, filename, options={}): # Store Options in the ZIP file fp.write(filename=filename + '.config', arcname='README') os.remove(filename + '.config') - + # Store matrices in the ZIP file for extension in ['.U', '.S', '.V']: fp.write(filename=filename + extension, arcname=extension) @@ -231,6 +237,7 @@ def _reconstruct_matrix(self, shifts=None, force=True): self._matrix_reconstructed = divisi2.reconstruct(self._U, self._S, self._V) return self._matrix_reconstructed + def compute(self, k=100, min_values=None, pre_normalize=None, mean_center=False, post_normalize=True, savefile=None): """ Computes SVD on matrix *M*, :math:`M = U \Sigma V^T` @@ -251,7 +258,7 @@ def compute(self, k=100, min_values=None, pre_normalize=None, mean_center=False, super(SVD, self).compute(min_values) #creates matrix and does squish to not have empty values if VERBOSE: - sys.stdout.write('Computing svd k=%s, min_values=%s, pre_normalize=%s, mean_center=%s, post_normalize=%s\n' + sys.stdout.write('Computing svd k=%s, min_values=%s, pre_normalize=%s, mean_center=%s, post_normalize=%s\n' % (k, min_values, pre_normalize, mean_center, post_normalize)) if not min_values: sys.stdout.write('[WARNING] min_values is set to None, meaning that some funky recommendations might appear!\n') @@ -264,17 +271,18 @@ def compute(self, k=100, min_values=None, pre_normalize=None, mean_center=False, if mean_center: if VERBOSE: sys.stdout.write("[WARNING] mean_center is True. svd.similar(...) might return nan's. If so, then do svd.compute(..., mean_center=False)\n") - matrix, row_shift, col_shift, total_shift = matrix.mean_center() + matrix, row_shift, col_shift, total_shift = matrix.mean_center() self._shifts = (row_shift, col_shift, total_shift) + # Pre-normalize input matrix? if pre_normalize: """ - Divisi2 divides each entry by the geometric mean of its row norm and its column norm. + Divisi2 divides each entry by the geometric mean of its row norm and its column norm. The rows and columns don't actually become unit vectors, but they all become closer to unit vectors. """ if pre_normalize == 'tfidf': - matrix = matrix.normalize_tfidf() #TODO By default, treats the matrix as terms-by-documents; + matrix = matrix.normalize_tfidf() #TODO By default, treats the matrix as terms-by-documents; # pass cols_are_terms=True if the matrix is instead documents-by-terms. elif pre_normalize == 'rows': matrix = matrix.normalize_rows() @@ -296,7 +304,7 @@ def compute(self, k=100, min_values=None, pre_normalize=None, mean_center=False, options = {'k': k, 'min_values': min_values, 'pre_normalize': pre_normalize, 'mean_center': mean_center, 'post_normalize': post_normalize} self.save_model(savefile, options) - def _get_row_reconstructed(self, i, zeros=None): + def _get_row_reconstructed(self, i, zeros=None): #if foldin that means it is known what the user rated and zeros contains the rated items if zeros: return self._matrix_reconstructed.row_named(i)[zeros] return self._matrix_reconstructed.row_named(i) @@ -306,6 +314,40 @@ def _get_col_reconstructed(self, j, zeros=None): return self._matrix_reconstructed.col_named(j)[zeros] return self._matrix_reconstructed.col_named(j) + def _get_row_unrated(self,i,rated): # use for foldin since that means users new rated items are known so no need to squish or need normal matrix + sparse_matrix = self._matrix_reconstructed.row_named(i).to_sparse() + # values: np array with the predicted ratings or ratings + # named_rows: normal array with movie names + values, named_cols = sparse_matrix.named_lists() #values contains a np array with predicted ratings , while named_cols contains list of labels of columns + removal_indicies = [] #array of indicies for removal + + for item in rated: + index_remove = named_cols.index(item) + del named_cols[index_remove] #since its a normal list can remove like this + removal_indicies.append(index_remove) + + values = np.delete(values, removal_indicies) #since it's a numpy array so must remove like this + + return divisiSparseVector.from_named_lists(values, named_cols).to_dense() + + + + def _get_col_unrated(self, j,rated): # use for foldin since that means users new rated items are known so no need to squish or need normal matrix + sparse_matrix=self._matrix_reconstructed.col_named(j).to_sparse() + # values: np array with the predicted ratings or ratings + # named_rows: normal array with movie names + values, named_rows= sparse_matrix.named_lists() + removal_indicies=[] + + for item in rated: + index_remove = named_rows.index(item) + del named_rows[index_remove] + removal_indicies.append(index_remove) + + values=np.delete(values, removal_indicies) + + return divisiSparseVector.from_named_lists(values,named_rows).to_dense() + def predict(self, i, j, MIN_VALUE=None, MAX_VALUE=None): """ Predicts the value of :math:`M_{i,j}`, using reconstructed matrix :math:`M^\prime = U \Sigma_k V^T` @@ -347,25 +389,79 @@ def recommend(self, i, n=10, only_unknowns=False, is_row=True): self.compute() #will use default values! item = None zeros = [] - if only_unknowns and not self._matrix.get(): + seeDict=False + if only_unknowns and not self._matrix.get() and len(self._foldinZeroes)==0: raise ValueError("Matrix is empty! If you loaded an SVD model you can't use only_unknowns=True, unless svd.create_matrix() is called") + if not self._matrix.get(): + seeDict=True if is_row: if only_unknowns: - zeros = self._matrix.get().row_named(i).zero_entries() - item = self._get_row_reconstructed(i, zeros) + if seeDict: + zeros=self._foldinZeroes[i] #zeros in this instance contains the rated items + if len(zeros)==0: + raise ValueError("Matrix is empty! If you loaded an SVD model you can't use only_unknowns=True, unless svd.create_matrix() is called or youve just folded them in") + else: + item = self._get_row_unrated(i, zeros) #removing the rated items from utility row for recommendations + else: + zeros = self._matrix.get().row_named(i).zero_entries() + item = self._get_row_reconstructed(i, zeros) + else: + item = self._get_row_reconstructed(i, zeros) else: if only_unknowns: - zeros = self._matrix.get().col_named(i).zero_entries() - item = self._get_col_reconstructed(i, zeros) + if seeDict: + zeros=self._foldinZeroes[i] #zeros in this instance contains the rated items + if len(zeros)==0: + raise ValueError("Matrix is empty! If you loaded an SVD model you can't use only_unknowns=True, unless svd.create_matrix() is called or you just folded them in") + else: + item = self._get_col_unrated(i, zeros) #removing the rated items from utility columns for recommendations + else: + zeros = self._matrix.get().col_named(i).zero_entries() + item = self._get_col_reconstructed(i, zeros) + else: + item = self._get_row_reconstructed(i, zeros) + return item.top_items(n) - def load_updateDataTuple_foldin(self, filename, force=True, sep='\t', format={'value':0, 'row':1, 'col':2}, pickle=False,is_row=True,truncate=False): + def _calc_mean_center(self, matrix, is_row=True): #created this to use the loaded shifts and calculate the row or column shift + row_shift, col_shift, total_shift = self._shifts + + total_mean = total_shift # use the global shift one + if is_row: + row_means = matrix.row_op(np.mean) - total_mean # calculate row shift + col_means = col_shift # use already given col shifts + else: + row_means = row_shift # use already given row shifts + col_means = matrix.col_op(np.mean) - total_mean # calculate col shifts + + row_lengths = matrix.row_op(len) + col_lengths = matrix.col_op(len) + + shifted = matrix.copy() + for row, col in shifted.keys(): + shifted[row, col] -= ( + (row_means[row] * row_lengths[row] + + col_means[col] * col_lengths[col] + ) / (row_lengths[row] + col_lengths[col]) + ) + total_mean + + return (shifted, row_means, col_means, total_mean) + # return shifted + + def load_updateDataTuple_foldin(self, filename,force=True, sep='\t', format={'value':0, 'row':1, 'col':2}, pickle=False,is_row=True,truncate=True,post_normalize=False): """ - Loads a dataset file that contains a SINGLE tuple (a dataset for a single user OR item , has to be either same row or same column depending on is_row aka tuple) + Folds-in a SINGLE user OR item. First loads a dataset file that contains a SINGLE tuple (a dataset for a single user OR item , has to be either same row or same column depending on is_row aka tuple) + + For params: filename,force,sep,format,pickle then see params definition in *datamodel.Data.load()* + + :param is_row: are you trying to foldin a row or a column ? yes->foldin row , no->foldin column + :type is_row: boolean + :param truncate: sometimes new users rate new items not in the original SVD matrix so would you like new items to be truncated or folded in ? default is foldin + :type truncate: boolean + :param post_normalize: Normalize every row of :math:`U \Sigma` to be a unit vector. Thus, row similarity (using cosine distance) returns :math:`[-1.0 .. 1.0]` + :type post_normalize: Boolean - See params definition in *datamodel.Data.load()* """ - # nDimension if force: self._updateData = Data() @@ -379,11 +475,14 @@ def load_updateDataTuple_foldin(self, filename, force=True, sep='\t', format={'v print type(nDimensionLabels[0]) print len(nDimensionLabels) self._singleUpdateMatrix.create(self._updateData.get(), col_labels=nDimensionLabels, foldin=True,truncate=truncate) + self._foldinZeroes[self._singleUpdateMatrix.get_rows()[0]] = self._singleUpdateMatrix.get_cols() + else: nDimensionLabels = self._U.all_labels() #get labels from U matrix to complete the sparse matrix print nDimensionLabels self._singleUpdateMatrix.create(self._updateData.get(), row_labels=nDimensionLabels, foldin=True,truncate=truncate) + self._foldinZeroes[self._singleUpdateMatrix.get_cols()[0]] = self._singleUpdateMatrix.get_rows() if not truncate: additionalElements=self._singleUpdateMatrix.get_additional_elements() @@ -402,23 +501,49 @@ def load_updateDataTuple_foldin(self, filename, force=True, sep='\t', format={'v # #update the data matrix if VERBOSE: print "updating the sparse matrix" - # print "matrix before update:",self._matrix.get().shape if self._matrix.get(): #if matrix not there due to load ignore it self._matrix.update(self._singleUpdateMatrix) # updating the data matrix for the zeroes , also for saving the data matrix if needed - # print "matrix after update:",self._matrix.get().shape - self._update(is_row=is_row) + + # Mean centering + if self._shifts: #if not None then it means mean_center was equal true + row_shift, col_shift, total_shift=self._shifts + + + meanedMatrix, rowShift, colShift, totalShift=self._calc_mean_center(self._singleUpdateMatrix.get(),is_row=is_row) + + self._singleUpdateMatrix.set(meanedMatrix) + + if is_row: + values, named_rows = row_shift.to_sparse().named_lists() #values numpy array, named_rows normal array + valuesFold, named_rowsFold = rowShift.to_sparse().named_lists() + + else: + values, named_rows = col_shift.to_sparse().named_lists() # values numpy array, named_rows normal array + valuesFold, named_rowsFold = colShift.to_sparse().named_lists() + + + values=np.concatenate((values, valuesFold)) + named_rows.extend(named_rowsFold) + + if is_row: + row_shift=divisiSparseVector.from_named_lists(values, named_rows).to_dense() + else: + col_shift=divisiSparseVector.from_named_lists(values, named_rows).to_dense() + + self._shifts=(row_shift, col_shift, total_shift) + + + self._update(is_row=is_row,post_normalize=post_normalize) def _construct_batch_dictionary(self,data,is_row=True): - ''' - + """ + :param data: Data() :param is_row: Boolean :return: constructs a dictionary with the row or col as the keys (depending on which is being added) with values as the tuples in self._batchDict - ''' - # self._values = map(itemgetter(0), data) - # self._rows = map(itemgetter(1), data) - # self._cols = map(itemgetter(2), data) + """ + key_idx=1 #key index default is the row if not is_row: key_idx=2 @@ -436,23 +561,33 @@ def _construct_batch_dictionary(self,data,is_row=True): print "Batch loaded successfully" - def load_updateDataBatch_foldin(self, filename, force=True, sep='\t', format={'value': 0, 'row': 1, 'col': 2}, - pickle=False, is_row=True,truncate=False): + def load_updateDataBatch_foldin(self, filename=None, data=None, force=True, sep='\t', format={'value': 0, 'row': 1, 'col': 2}, + pickle=False, is_row=True,truncate=True,post_normalize=False): """ - Dont forget future work in presentation , remove old and insert new - Loads a dataset file that contains Multiple tuples + Folds in the batch users or items, first Loads a dataset file that contains Multiple tuples (users or items) or uses the preloaded data from the datamodel/data.py object then folds them in with their ratings + + :param data: Contains the dataset that was loaded using the Data() class + :type data: Data() - truncate:boolean-> sometimes new users rate new items not in the original SVD matrix so would you like new items to be truncated or folded in ? default is foldin - is_row: boolean -> are you trying to foldin a row or a column ? yes->foldin row , no->foldin column - See params definition in *datamodel.Data.load()* - + For params: filename,force,sep,format,pickle then see params definition in *datamodel.Data.load()* + + :param is_row: are you trying to foldin a row or a column ? yes->foldin row , no->foldin column + :type is_row: boolean + :param truncate: sometimes new users rate new items not in the original SVD matrix so would you like new items to be truncated or folded in ? default is foldin + :type truncate: boolean + :param post_normalize: Normalize every row of :math:`U \Sigma` to be a unit vector. Thus, row similarity (using cosine distance) returns :math:`[-1.0 .. 1.0]` + :type post_normalize: Boolean """ - # call update here until it finishes - # nDimension + if force: self._updateData = Data() - - self._updateData.load(filename, force, sep, format, pickle) #load array of tuples + if filename: #not null + self._updateData.load(filename, force, sep, format, pickle) #load array of tuples + else: + if data: + self._updateData =data + else: + raise ValueError('No data or filename set!') print "Reading the new batch" self._construct_batch_dictionary(self._updateData.get(),is_row) @@ -461,32 +596,32 @@ def load_updateDataBatch_foldin(self, filename, force=True, sep='\t', format={'v nDimensionLabels=None if (is_row): nDimensionLabels = self._V.all_labels()[0] # get labels from V matrix to complete the sparse matrix - # print nDimensionLabels else: nDimensionLabels = self._U.all_labels()[0] # get labels from U matrix to complete the sparse matrix - # print nDimensionLabels length_of_dict=len(self._batchDict) i=0 + meanDenseVector=[] isbatch=True for key_idx in self._batchDict: #data in batchDict in form {key:[(tuple)]} - print "user:",key_idx i += 1 + if VERBOSE: + if i % 100 == 0: + sys.stdout.write('.') + if i % 1000 == 0: + sys.stdout.write('|') + if i % 10000 == 0: + sys.stdout.write(' (%d K user)\n' % int(i / 1000)) + if (is_row): self._singleUpdateMatrix.create(self._batchDict[key_idx], col_labels=nDimensionLabels,foldin=True,truncate=truncate) else: self._singleUpdateMatrix.create(self._batchDict[key_idx], row_labels=nDimensionLabels,foldin=True,truncate=truncate) - # if(i==length_of_dict): - # isbatch=False - - # If it's trying to foldin a new user who has rated a new item which was not used before, then foldin the item first then foldin that user if not truncate: additionalElements = self._singleUpdateMatrix.get_additional_elements() - print "dimension", len(nDimensionLabels) - print "additional elements:", additionalElements - print "length", len(additionalElements) + if len(additionalElements) != 0: for item in additionalElements: if (is_row): # if I am folding in a row then , the additionals added that shouldn't be are the columns to be folded in to the rows @@ -495,24 +630,54 @@ def load_updateDataBatch_foldin(self, filename, force=True, sep='\t', format={'v else: self._singleAdditionalFoldin.create([(0, item, nDimensionLabels[0])], col_labels=self._V.all_labels()[0]) + self._update(update_matrix=self._singleAdditionalFoldin, is_row=not is_row) + if self._shifts: # if not None then it means mean_center was equal true + row_shift, col_shift, total_shift = self._shifts + + + meanedMatrix, rowShift, colShift, totalShift = self._calc_mean_center(self._singleUpdateMatrix.get(),is_row=is_row) + + self._singleUpdateMatrix.set(meanedMatrix) + # row shift cause it's row for the time being + if is_row: + meanDenseVector.append(rowShift) + + else: + meanDenseVector.append(colShift) + - # #update the data matrix - print "updating the sparse matrix" - # print "matrix before update:",self._matrix.get().shape if self._matrix.get(): #if matrix not there due to load ignore it self._matrix.update( self._singleUpdateMatrix,is_batch=isbatch) # updating the data matrix for the zeroes , also for saving the data matrix if needed - # print "matrix after update:",self._matrix.get().shape + self._update(is_row=is_row,is_batch=isbatch) #Do foldin on the singleUpdateMatrix tuple + if VERBOSE: + sys.stdout.write('\n') + # UPDATING MEAN CENTER PART + if self._shifts: + sys.stdout.write("updating shifts") + if is_row: + values, named_rows = row_shift.to_sparse().named_lists() # values numpy array, named_rows normal array + else: + values, named_rows = col_shift.to_sparse().named_lists() # values numpy array, named_rows normal array + for vector in meanDenseVector: + valuesFold, named_rowsFold = vector.to_sparse().named_lists() # rowShift contains new calculated row shift + values = np.concatenate((values, valuesFold)) + named_rows.extend(named_rowsFold) + if is_row: + row_shift = divisiSparseVector.from_named_lists(values, named_rows).to_dense() + else: + col_shift = divisiSparseVector.from_named_lists(values, named_rows).to_dense() - self.update_sparse_matrix_data(is_batch=True) + self._shifts = (row_shift, col_shift, total_shift) + self.update_sparse_matrix_data(is_batch=True,squish=False,post_normalize=post_normalize) - def update_sparse_matrix_data(self,squishFactor=10,is_batch=False): + + def update_sparse_matrix_data(self,squishFactor=10,is_batch=False,squish=True,post_normalize=False): #update the data matrix - # print "matrix before update:",self._matrix.get().shape if is_batch: if self._matrix.get(): if VERBOSE: @@ -521,48 +686,34 @@ def update_sparse_matrix_data(self,squishFactor=10,is_batch=False): if VERBOSE: print "before updating, M=", self._matrix_reconstructed.shape # Sim. matrix = U \Sigma^2 U^T - self._reconstruct_similarity(post_normalize=False, force=True) + self._reconstruct_similarity(post_normalize=post_normalize, force=True) # M' = U S V^t self._reconstruct_matrix(shifts=self._shifts, force=True) if VERBOSE: print "done updating, M=", self._matrix_reconstructed.shape + if squish: + if self._matrix.get(): #if loaded model there is no matrix + if VERBOSE: + print "commiting the sparse data matrix by removing empty rows and columns divisi created" + self._matrix.squish(squishFactor) # updating the data matrix for the zeroes ,#NOTE: Intensive so do at end - if self._matrix.get(): #if loaded model there is no matrix - if VERBOSE: - print "commiting the sparse data matrix by removing empty rows and columns divisi created" - self._matrix.squish(squishFactor) # updating the data matrix for the zeroes ,#NOTE: Intensive so do at end - # print "matrix after update:",self._matrix.get().shape - - - def _update(self,update_matrix=None,is_row=True,is_batch=False): #update(tuple:denseVector tuple,isRow=True,, - if VERBOSE: - print "type of S",type(self._S) - print "type of U",type(self._U) - print "type of V",type(self._V) - print "type of data",type(self._data) - print "type of matrix",type(self._matrix) - print "type of matrix reconstructed",type(self._matrix_reconstructed) - print "type of matrix similarity",type(self._matrix_similarity) - - print "dimensions of S",self._S.shape - print "dimensions of U",self._U.shape - print "dimensions of V",self._V.shape - - invS=np.zeros((self._S.shape[0], self._S.shape[0])) - for i in range(self._S.shape[0]): - # invS[i, i] = self._S[i] # creating diagonal matrix - invS[i, i] = self._S[i]**-1 # creating diagonal matrix and inverting using special property of diagonal matrix - # invS=inv(invS) inverting with numpy + + def _update(self,update_matrix=None,is_row=True,is_batch=False,post_normalize=False): + #The function which does the actual folding-in process + if self.inv_S is None: + self.inv_S=np.zeros((self._S.shape[0], self._S.shape[0])) + for i in range(self._S.shape[0]): + self.inv_S[i, i] = self._S[i]**-1 # creating diagonal matrix and inverting using special property of diagonal matrix #if new is row -> V*S^-1 if is_row: - prodM=self._V.dot(invS) - if VERBOSE: - print "dimension of VxS^-1=", prodM.shape + prodM=self._V.dot(self.inv_S) + # if VERBOSE: + # print "dimension of VxS^-1=", prodM.shape else: #if new is col -> U*S^-1 - prodM = self._U.dot(invS) - if VERBOSE: - print "dimension of UxS^-1=", prodM.shape + prodM = self._U.dot(self.inv_S) + # if VERBOSE: + # print "dimension of UxS^-1=", prodM.shape if update_matrix: updateTupleMatrix=update_matrix.get() @@ -571,74 +722,30 @@ def _update(self,update_matrix=None,is_row=True,is_batch=False): #update(tuple:d if not is_row: updateTupleMatrix=updateTupleMatrix.transpose() #transpose - if VERBOSE: - print "dimensions of user",updateTupleMatrix.shape + res=updateTupleMatrix.dot(prodM) - if VERBOSE: - print "type of res=", type(res) - print "dimension of resultant is", res.shape if is_row: - #use new value can now be concatinated with U - if VERBOSE: - print "U before adding", self._U.shape + #new value can now be concatinated with U + self._U=self._U.concatenate(res) - if VERBOSE: - print "U after adding", self._U.shape else: - if VERBOSE: - print "V before adding", self._V.shape + #new value can now be concatinated with V + self._V = self._V.concatenate(res) - if VERBOSE: - print "V after adding", self._V.shape - #TODO: contemplating removing this segment and just reconstruct in the updating spare matrix function if not is_batch: #will reconstruct all at end with batch using another function if VERBOSE: print "before updating, M=",self._matrix_reconstructed.shape # Sim. matrix = U \Sigma^2 U^T - self._reconstruct_similarity(post_normalize=False, force=True) + self._reconstruct_similarity(post_normalize=post_normalize, force=True) # M' = U S V^t self._reconstruct_matrix(shifts=self._shifts, force=True) if VERBOSE: print "done updating, M=",self._matrix_reconstructed.shape - - - # myFile=open("prodMVSq.dat",'w') - # myFile.truncate() - # - # for i in range(20): - # myFile.write(str(res[0, i])+" ") - # - # myFile.write("\n") - - # # invS = inv(diag_S) - # # print "dimensions of S^-1", invS.shape - # - # - # print "writing s to file" - # myFile=open("invS.dat",'w') - # myFile.truncate() - # # for item in self.invS.tolist(): - # # myFile.write(str(item)) - # # myFile.write("\n") - # myFile.write("dimensions= "+str(invS.shape)) - # myFile.write("\n") - # for i in range(invS.shape[0]): - # myFile.write(str(invS[i,i])) - # myFile.write("\n") - - def printMovies(self): - myFile=open("movieIDs.dat",'w') - myFile.truncate() - - movies=self._matrix_reconstructed.get_col_labels() - for movie in movies : - myFile.write(str(movie)+",") - def centroid(self, ids, is_row=True): points = [] for id in ids: @@ -684,7 +791,7 @@ def kmeans(self, ids, k=5, components=3, are_rows=True): i = 0 clusters = dict() for cluster in labels: - if not clusters.has_key(cluster): + if not clusters.has_key(cluster): clusters[cluster] = dict() clusters[cluster]['centroid'] = centroids[cluster] clusters[cluster]['points'] = [] @@ -754,7 +861,7 @@ def similar_neighbours(self, i, j, Sk=10): _Sk += 1 current += 1 _Sk -= 1 - if _Sk == 0: + if _Sk == 0: break # We have enough elements to use return similars[:Sk] @@ -816,7 +923,7 @@ def predict(self, i, j, Sk=10, weighted=True, MIN_VALUE=None, MAX_VALUE=None): # SVDNeighbourhoodKoren class __SVDNeighbourhoodKoren(SVDNeighbourhood): """ - Inherits from SVDNeighbourhood class. + Inherits from SVDNeighbourhood class. Neighbourhood model, using Singular Value Decomposition. Based on 'Factorization Meets the Neighborhood: a Multifaceted @@ -901,7 +1008,7 @@ def predict(self, i, j, Sk=None, MIN_VALUE=None, MAX_VALUE=None): Predicts the value of *M(i,j)* It is based on 'Factorization Meets the Neighborhood: a Multifaceted - Collaborative Filtering Model' (Yehuda Koren). + Collaborative Filtering Model' (Yehuda Koren). Equation 3 (section 2.2): :math:`\hat{r}_{ui} = b_{ui} + \\frac{\sum_{j \in S^k(i;u)} s_{ij} (r_{uj} - b_{uj})}{\sum_{j \in S^k(i;u)} s_{ij}}`, where @@ -925,8 +1032,8 @@ def predict(self, i, j, Sk=None, MIN_VALUE=None, MAX_VALUE=None): # bui = µ + bu + bi # The parameters bu and bi indicate the observed deviations of user # u and item i, respectively, from the average - # - # S^k(i; u): + # + # S^k(i; u): # Using the similarity measure, we identify the k items rated # by u, which are most similar to i. # @@ -946,7 +1053,7 @@ def predict(self, i, j, Sk=None, MIN_VALUE=None, MAX_VALUE=None): bui = bu + bi #if self._Mu: #TODO uncomment? # bui += self._Mu - + sim_ratings = [] sum_similarity = 0.0 for similar, sij in similars[1:]: @@ -965,10 +1072,9 @@ def predict(self, i, j, Sk=None, MIN_VALUE=None, MAX_VALUE=None): Sumj_Sk = sum(sim_ratings)/sum_similarity rui = bui + Sumj_Sk predicted_value = rui - + if MIN_VALUE: predicted_value = max(predicted_value, MIN_VALUE) if MAX_VALUE: predicted_value = min(predicted_value, MAX_VALUE) return float(predicted_value) - diff --git a/recsys/algorithm/factorize.pyc b/recsys/algorithm/factorize.pyc index 79f7199fc03ca842f12de55634fa87558ea13eb6..cc7a947dcb9f76cedf3a58fec1a2f623f818422f 100644 GIT binary patch delta 9576 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 NHGiP^_mSp1{vS;i@lyZ* delta 7122 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 Mch$S<g|?Ib0|i`J`Tzg` diff --git a/recsys/algorithm/matrix.py b/recsys/algorithm/matrix.py index b4fc51e..36e0313 100644 --- a/recsys/algorithm/matrix.py +++ b/recsys/algorithm/matrix.py @@ -88,16 +88,11 @@ def get_cols(self): #can use to get rated items and remove from recommendation def get_additional_elements(self): # can use to get rated items and remove from recommendation return self._additional_elements - # def create_blank(self,element_id,row_labels=None, col_labels=None): - # if col_labels: - # - # self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols,row_labels, col_labels) - - #row_labels specifies the row labels the complete matrix should have incase the inputted file doesn't include all indicies and it was saved in previous matrix (for update) #same explination for col_labels but for columns #matrix should have, in case it is larger than the largest index. - def create(self, data,row_labels=None, col_labels=None, foldin=False,truncate=False): #is_row is what I'm originally folding in + def create(self, data,row_labels=None, col_labels=None, foldin=False,truncate=False): + #is_row is what I'm originally folding in self._values = map(itemgetter(0), data) self._rows = map(itemgetter(1), data) self._cols = map(itemgetter(2), data) @@ -154,4 +149,3 @@ def empty(self): if self._matrix: nrows, ncols = self._matrix.shape return not self._matrix or not (nrows and ncols) - diff --git a/recsys/datamodel/data.py b/recsys/datamodel/data.py index f2f3d94..de4a5e8 100644 --- a/recsys/datamodel/data.py +++ b/recsys/datamodel/data.py @@ -17,6 +17,7 @@ def __init__(self): #:type data: list #""" self._data = list([]) + self._tupleDict = {} def __repr__(self): s = '%d rows.' % len(self.get()) @@ -52,6 +53,13 @@ def get(self): :returns: a list of tuples """ return self._data + def get_tuple_dict(self): + """ + :returns: a dictionary of users or items and corresponding ratings + """ + if not self._tupleDict: + raise ValueError('Tuple dictionary hasn\'t been created yet, please run split_train_test_foldin first then try again') + return self._tupleDict def add_tuple(self, tuple): """ @@ -94,6 +102,166 @@ def split_train_test(self, percent=80, shuffle_data=True): return train, test + def split_train_test_foldin(self,base=60,percentage_base_user=80, shuffle_data=True,is_row=True,force=True,data_report_path=None,id=None,ignore_rating_count=0): + """ + Splits the data in three datasets: train, test, and foldin + + :param base: % of training set to be used (Foldin set size = 100-base) for base SVD model (not folded) + :type base: int + :param percentage_base_user: % of user ratings per user (or item ratings per item depending on which is row and column) to be used as base for training or foldin (testing will be percentage of ratings from 100-percentage_base_user per user or item ) + :type percentage_base_user: int + + :param shuffle_data: shuffle dataset? + :type shuffle_data: Boolean + + :param is_row: are you trying to foldin a row or a column ? yes-> row , no-> column + :type is_row: Boolean + :param force: clear the values in data + :type force: Boolean + + + The following parameters are used for when generating a report of the dataset distribution: + :param data_report_path: path to create report in + :type data_report_path: String + :param id: id number to be given to the report + :type id: String + :param ignore_rating_count: shuffle dataset? + :type ignore_rating_count: Boolean + + :returns: a tuple <Data, Data, Data> for train, test, foldin + """ + if force: + self._construct_dictionary(is_row=is_row,force=True) + elif len(self._tupleDict)==0: + self._construct_dictionary(is_row=is_row) + self._remove_ratings_count_from_dictionary(ignore_rating_count) + dictKeys=self._tupleDict.keys() #users + numberOfKeys= len(dictKeys) #number of users + + train_list =[] + test_list=[] + foldin_list=[] + + if shuffle_data: + shuffle(dictKeys) + train_list_keys=dictKeys[:int(round(numberOfKeys*base/100.0))] + if base==100: + foldin_list_keys=[] + else: + foldin_list_keys=dictKeys[-int(round(numberOfKeys*(100-base)/100.0)):] + + for key in train_list_keys: + tupleList=self._tupleDict[key] + lengthTupleList=len(tupleList) + if shuffle_data: + shuffle(tupleList) + + train_list.extend(tupleList[:int(round(lengthTupleList*percentage_base_user/100.0))]) + if int(round(lengthTupleList*(100-percentage_base_user)/100.0)) !=0: #if test=0 then can't take that percentage so skip taking it's tuple for test + test_list.extend(tupleList[-int(round(lengthTupleList*(100-percentage_base_user)/100.0)):]) + + for key in foldin_list_keys: + tupleList=self._tupleDict[key] + lengthTupleList=len(tupleList) + if shuffle_data: + shuffle(tupleList) + + foldin_list.extend(tupleList[:int(round(lengthTupleList*percentage_base_user/100.0))]) + if int(round(lengthTupleList*(100-percentage_base_user)/100.0)) !=0: #if test=0 then can't take that percentage so skip taking it's tuple for test + test_list.extend(tupleList[-int(round(lengthTupleList*(100-percentage_base_user)/100.0)):]) + + + + length = len(self._data) + if VERBOSE: + print "total number of tuples:",length + print "percentage of data for training:",round((len(train_list)*1.0/length)*100),"%","with",len(train_list),"tuples" + print "percentage of data for testing:",round((len(test_list)*1.0/length)*100),"%","with",len(test_list),"tuples" + print "percentage of data for foldin:",round((len(foldin_list)*1.0/length)*100),"%","with",len(foldin_list),"tuples" + print "_____________" + print "percentage of users for foldin:",round((len(foldin_list_keys)*1.0/numberOfKeys*1.0)*100),"%","with",len(foldin_list_keys),"users" + print "percentage of users for training:",round((len(train_list_keys)*1.0/numberOfKeys*1.0)*100),"%","with",len(train_list_keys),"users" + + if data_report_path: + myFile = open(data_report_path+"/data_distribution_report.txt", 'a+') + + myFile.write("DataID:"+ str(id)) + myFile.write("total number of tuples:"+ str(length)) + myFile.write("\n") + myFile.write( "percentage of data for training:"+ str(round((len(train_list) * 1.0 / length) * 100))+ "%"+ "with"+str(len(train_list))+"tuples") + myFile.write("\n") + myFile.write( "percentage of data for testing:"+ str(round((len(test_list) * 1.0 / length) * 100))+ "%"+ "with"+ str(len(test_list))+ "tuples") + myFile.write("\n") + myFile.write("percentage of data for foldin:"+ str(round((len(foldin_list) * 1.0 / length) * 100))+ "%"+ "with"+ str(len(foldin_list))+ "tuples") + myFile.write("\n") + myFile.write("_____________") + myFile.write("\n") + myFile.write("percentage of users for foldin:"+ str(round((len(foldin_list_keys) * 1.0 / numberOfKeys * 1.0) * 100))+ "%"+ "with"+ str(len(foldin_list_keys))+ "users") + myFile.write("\n") + myFile.write("percentage of users for training:"+ str(round((len(train_list_keys) * 1.0 / numberOfKeys * 1.0) * 100))+ "%"+"with"+ str(len(train_list_keys))+ "users") + myFile.write("\n") + myFile.write("________________________________________________________________") + myFile.write("\n") + + myFile.close() + + + train = Data() + train.set(train_list) + test = Data() + test.set(test_list) + foldin=Data() + foldin.set(foldin_list) + + return train, test, foldin + + def _remove_ratings_count_from_dictionary(self,count_threshold_to_remove): + ''' + :param count_threshold_to_remove: The threshold number of ratings to be removed from the data. + :type count_threshold_to_remove: int + :return: void, it changes the data itself in the class. + ''' + if count_threshold_to_remove==0: + return + removed=0 + dictKeys=self._tupleDict.keys() + for key in dictKeys: + if len(self._tupleDict[key])<=count_threshold_to_remove: + del self._tupleDict[key] + removed+=1 + + print "users removed less than or equal threshold count=",removed,"users" + return + + def _construct_dictionary(self, is_row=True,force=True): + ''' + + :param data: Data() + :param is_row: Boolean + :return: constructs a dictionary with the row or col as the keys (depending on which is being added) with values as the tuples + in self._batchDict + ''' + # self._values = map(itemgetter(0), data) + # self._rows = map(itemgetter(1), data) + # self._cols = map(itemgetter(2), data) + key_idx = 1 # key index default is the row + if not is_row: + key_idx = 2 + if force: #construct new dictionary + self._tupleDict={} + # collecting the significant col or row tuples at one place to fold them in at once + + for item in self._data: # data is a list of tuples so item is 1 tuple + try: + self._tupleDict[item[key_idx]].append(item) + except KeyError: + self._tupleDict[item[key_idx]] = [] + self._tupleDict[item[key_idx]].append(item) + + # batch loaded , now need to fold them in one by one + if VERBOSE: + print "Dictionary created successfully" + def load(self, path, force=True, sep='\t', format=None, pickle=False): """ Loads data from a file @@ -104,10 +272,10 @@ def load(self, path, force=True, sep='\t', format=None, pickle=False): :type force: Boolean :param sep: Separator among the fields of the file content :type sep: string - :param format: Format of the file content. + :param format: Format of the file content. Default format is 'value': 0 (first field), then 'row': 1, and 'col': 2. - E.g: format={'row':0, 'col':1, 'value':2}. The row is in position 0, - then there is the column value, and finally the rating. + E.g: format={'row':0, 'col':1, 'value':2}. The row is in position 0, + then there is the column value, and finally the rating. So, it resembles to a matrix in plain format :type format: dict() :param pickle: is input file in pickle format? @@ -120,7 +288,7 @@ def load(self, path, force=True, sep='\t', format=None, pickle=False): if pickle: self._load_pickle(path) else: - i = 0 + i = 0 for line in codecs.open(path, 'r', 'ISO-8859-1'): #was utf8 changed it to 'ISO-8859-1' data = line.strip('\r\n').split(sep) value = None @@ -140,7 +308,7 @@ def load(self, path, force=True, sep='\t', format=None, pickle=False): value = data[format['value']] except KeyError, ValueError: value = 1 - try: + try: row_id = data[format['row']] except KeyError: row_id = data[1] diff --git a/recsys/datamodel/data.pyc b/recsys/datamodel/data.pyc index 176bf003ceacb312e03caca2d384a9ec88644cf4..cae0024d2cec36ea7ac9f0508473dc791dccbe96 100644 GIT binary patch delta 5076 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 delta 1097 zcmZ8g&1(}u6o0cxHraI3q=}(OYg@4<Ta@&}4=NOmtrcn^lxYPiNL;d06Psq!&88Z# zC>lI@DRZ-W5TqAHkX(B8Ry~MN75@P}TET;$7w64JkskKPoA=)Q=J!5!n>{@r^8JW} zx8D4`vJv_a4gkQU_-dOQU$0Iv9jq`k6<CY_+8toUfQ7))q3MUkC|D6_0xWvYPG}NX z>;fw)opGS^Mi<{9^QRJm(FM2;aG$_!fII;UVBR3YpEDGU`UR9rHP=N2dCPQ7UR3U? z7=r-+t~@6yf9zW$NuKae#zMkNH35=>syrJi@EyNSdiY2G+(8*eB1xOGD@;Bj^3Dbt zq>pb0W;*uYw$%tZ!q?SLQT6XdR6!HSt+_totT8vm-vw8Nlx)a~BEuNErAU8kPn%On zh-dY5Pp5EGDFDgLYL!(i1J6RjJ$;jmxB4REN*Z-a=2FEg+_CEwi{e`}DZr40sC+&8 zo?PO&SZ`}@VAV%Hx6HvagbeUUN4ErKQy0iUtC<QCQU1g9VbafErgy`#lQ*4`%lHE$ zrO8&<LI{6lB=4X=7YkTuRN1uS*p7>D&9a7f2#Q0|tmT(kgZ5)?SinrhVz*?R_Ms08 zS}NP7Yea-a|MjhEx<xN&I|bHmUDd_-lI^URt~-QwwNzLxGa40Ud;Aqf4<J(yLNCH0 zZVs&yv$Z=kO9r#(!R@3c5KbbXhW2prIDwN<aVoE&_2Sryk{U+$2*RiUR~L}4nQJWH ze%_<v=Q7EeZp^|$rJNhKm<rMt%65&lM}Zj=2w4HLGP~~nuNPw-avgtXH03RDb@*3i zN_cYyuDEv(Evvp#mFi_3`%8#Vi<EET^Oh@^GZOsS$nncK7m77d+42}#aRE~3MYCLE za&hFp;SCAjT5#%&8kj2=Q??L$L@yys^UPS0KN$_5#+Iu-m1sdt)qGk&^Xa;<;?eHp XHBHk8kpc|t5S49}z02}1qq~0q$q3K) From 9ab96c699cf99f46d4c6556d9357fa6504eb9812 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <Ibrahim.elseoud@gmail.com> Date: Thu, 8 Jun 2017 16:29:06 +0200 Subject: [PATCH 4/7] updating comments and documentation --- recsys/algorithm/matrix.py | 4 ++-- recsys/datamodel/data.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/recsys/algorithm/matrix.py b/recsys/algorithm/matrix.py index 36e0313..253a07f 100644 --- a/recsys/algorithm/matrix.py +++ b/recsys/algorithm/matrix.py @@ -85,7 +85,7 @@ def get_rows(self): #can use to get rated items and remove from recommendation def get_cols(self): #can use to get rated items and remove from recommendation return self._cols - def get_additional_elements(self): # can use to get rated items and remove from recommendation + def get_additional_elements(self): # can use to get additional items to either fold or truncate return self._additional_elements #row_labels specifies the row labels the complete matrix should have incase the inputted file doesn't include all indicies and it was saved in previous matrix (for update) @@ -131,7 +131,7 @@ def update(self, matrix,is_batch=False): #isbatch is for creating the final spar def squish(self,squishFactor): #remove additional empty fields created by divisiSparseMatrix self._matrix=self._matrix.squish(squishFactor) - def index_sparseMatrix(self): + def index_sparseMatrix(self): #create the divisi2 sparse matrix from already existing values self._matrix = divisiSparseMatrix.from_named_lists(self._values, self._rows, self._cols) def empty(self): diff --git a/recsys/datamodel/data.py b/recsys/datamodel/data.py index de4a5e8..6919229 100644 --- a/recsys/datamodel/data.py +++ b/recsys/datamodel/data.py @@ -125,8 +125,8 @@ def split_train_test_foldin(self,base=60,percentage_base_user=80, shuffle_data=T :type data_report_path: String :param id: id number to be given to the report :type id: String - :param ignore_rating_count: shuffle dataset? - :type ignore_rating_count: Boolean + :param ignore_rating_count: The threshold number of ratings to be removed from the data. + :type ignore_rating_count: int :returns: a tuple <Data, Data, Data> for train, test, foldin """ From af7c0a210dcc9414dddae2bd53dc45e63b03ca24 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <Ibrahim.elseoud@gmail.com> Date: Mon, 14 Aug 2017 15:42:15 +0200 Subject: [PATCH 5/7] Updated the README.rst to include the incremental svd update --- README.rst | 160 ++++++++++++++++++++++++++++--- recsys/__init__.pyc | Bin 556 -> 0 bytes recsys/algorithm/__init__.pyc | Bin 251 -> 0 bytes recsys/algorithm/baseclass.pyc | Bin 11617 -> 0 bytes recsys/algorithm/factorize.pyc | Bin 32828 -> 0 bytes recsys/algorithm/matrix.pyc | Bin 8411 -> 0 bytes recsys/datamodel/__init__.pyc | Bin 226 -> 0 bytes recsys/datamodel/data.pyc | Bin 12984 -> 0 bytes recsys/evaluation/__init__.pyc | Bin 269 -> 0 bytes recsys/evaluation/baseclass.pyc | Bin 4781 -> 0 bytes recsys/evaluation/prediction.pyc | Bin 3802 -> 0 bytes 11 files changed, 147 insertions(+), 13 deletions(-) delete mode 100644 recsys/__init__.pyc delete mode 100644 recsys/algorithm/__init__.pyc delete mode 100644 recsys/algorithm/baseclass.pyc delete mode 100644 recsys/algorithm/factorize.pyc delete mode 100644 recsys/algorithm/matrix.pyc delete mode 100644 recsys/datamodel/__init__.pyc delete mode 100644 recsys/datamodel/data.pyc delete mode 100644 recsys/evaluation/__init__.pyc delete mode 100644 recsys/evaluation/baseclass.pyc delete mode 100644 recsys/evaluation/prediction.pyc diff --git a/README.rst b/README.rst index 27e9d66..1444df5 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,19 @@ python-recsys A python library for implementing a recommender system. +- Now supports incrementally adding new users or items instead of building the model from scratch for these new users or items via the folding-in technique which was mentioned in Sarwar et al.'s `paper`_ (Titled: Incremental Singular Value Decomposition Algorithms for Highly Scalable Recommender Systems), this latest commit is simply an implementation to it for python-recsys. + +.. _`paper`: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.3.7894&rep=rep1&type=pdf + +- A `Demonstration video is available`_ for this latest commit in form of a demo site built using the MEAN stack which uses the updated python-recsys as backend for the recommender which folds-in the website's user in to the SVD model and gets recommendations instantaneously instead of building the model from scratch. + +.. _`Demonstration video is available`: https://youtu.be/tIvQxBfa2d4 + +-There is also an accompanying `bachelor thesis paper`_ (For those interested) which outlines the background, architecture and discusses the "Folding-in" approach. + +.. _`bachelor thesis paper`: https://drive.google.com/file/d/0BylQe2cRVWE_RmZoUTJYSGZNaXM/view + + Installation ============ @@ -57,8 +70,8 @@ Example from recsys.algorithm.factorize import SVD svd = SVD() - svd.load_data(filename='./data/movielens/ratings.dat', - sep='::', + svd.load_data(filename='./data/movielens/ratings.dat', + sep='::', format={'col':0, 'row':1, 'value':2, 'ids': int}) 2. Compute Singular Value Decomposition (SVD), M=U Sigma V^t: @@ -66,11 +79,11 @@ Example :: k = 100 - svd.compute(k=k, - min_values=10, - pre_normalize=None, - mean_center=True, - post_normalize=True, + svd.compute(k=k, + min_values=10, + pre_normalize=None, + mean_center=True, + post_normalize=True, savefile='/tmp/movielens') 3. Get similarity between two movies: @@ -111,10 +124,10 @@ Example USERID = 1 svd.predict(ITEMID, USERID, MIN_RATING, MAX_RATING) - # Predicted value 5.0 + # Predicted value 5.0 svd.get_matrix().value(ITEMID, USERID) - # Real value 5.0 + # Real value 5.0 6. Recommend (non-rated) movies to a user: @@ -152,7 +165,130 @@ Example (4801, 5.4947999354188548), (1131, 5.4941438045650068), (2339, 5.4916048051511659)] - + + +Example for incremental update +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1. Load Movielens dataset and prepare for training and testing: + +:: + + import recsys.algorithm + recsys.algorithm.VERBOSE = True + + from recsys.algorithm.factorize import SVD + from recsys.datamodel.data import Data + + filename = “(your movielens file path here)” + + #In movielens dataset, the user is at 0 so I made them the row (could keep it as above {'col':0, 'row':1, 'value':2, 'ids': int} but I changed order to emphasis a parameter in an upcoming function) + format = {'col':1, 'row':0, 'value':2, 'ids': int} + + data = Data() + data.load(filename, sep='::', format=format) + #splits the dataset according to row or column (based on is_row=true or false) which causes there to be no overlap (of users for example) between train and foldin dataset + train, test, foldin = data.split_train_test_foldin(base=60,percentage_base_user=80,shuffle_data=True,is_row=True) #since users are in the row so is_row=true + + # Returns: a tuple <Data, Data, Data> for train, test, foldin + # Prints: (If VERBOSE=True) + total number of tuples: 1000209 + percentage of data for training: 48.0 % with 479594 tuples + percentage of data for testing: 20.0 % with 200016 tuples # 100-percentage_base_user per user (percentage of tuples which means the ratings since a user has many tuples(ratings)) + percentage of data for foldin: 32.0 % with 320599 tuples + _____________ + percentage of users for foldin: 40.0 % with 2416 users # 100-base= foldin (percentage of users) + percentage of users for training: 60.0 % with 3624 users #base for training (percentage of users) + +2. Compute Singular Value Decomposition (SVD), M=U Sigma V^t: + +:: + + svd = SVD() + svd.set_data(train) + svd.compute(k=100, + min_values=1, + pre_normalize=None, + mean_center=False, + post_normalize=True) + + # Prints: + Creating matrix (479594 tuples) + Matrix density is: 3.7007% + Updating matrix: squish to at least 1 values + Computing svd k=14, min_values=1, pre_normalize=None, mean_center=False, post_normalize=False + +3. "Foldin" those new users or items (update model instead of updating from scratch) + +:: + + svd.load_updateDataBatch_foldin(data=foldin,is_row=True) + + # Prints: (If VERBOSE=True) + before updating, M= (3624, 3576) + done updating, M= (6040, 3576) # Folds in all the new users (not previously in model) + +4. Recommend (non-rated) movies to a NEW user +:: + + user_id=foldin[0][1] #returns userID which is in foldin dataset BUT not in train dataset + svd.recommend(user_id,is_row=True,only_unknowns=True) #The userID is in row and gets only the unrated (unknowns) + + # Returns: <ITEMID, Predicted Rating> + [(1307, 3.6290483094468913), + (1394, 3.5741565545425957), + (1259, 3.5303836262378048), + (1968, 3.4565426585553927), + (2791, 3.3470277643217203), + (1079, 3.268283171487782), + (1198, 3.2381080336246675), + (593, 3.204915630088236), + (1270, 3.1859618303393233), + (2918, 3.1548530640630252)] + +5. Recommend (non-rated) movies to a NEW user and validate not in base model (prior to folding-in) +:: + + # BEFORE running points 3 and 4 (prior to calling svd.load_updateDataBatch_foldin) + + user_id=foldin[0][1] #returns userID which is in foldin dataset BUT not in train dataset + + # Try block to validate that the userID is new and not in the base model + try: + print "Getting recommendation for user_id which was not in original model training set" + print "recommendations:",svd.recommend(user_id) + except Exception: + print "New user not in base model so in except block and will foldin the foldin dataset (update the model NOT calculate from scratch)" + svd.load_updateDataBatch_foldin(data=foldin,format=format,is_row=True,truncate=True,post_normalize=True) + print "recommendations:",svd.recommend(user_id,is_row=True,only_unknowns=True) #The userID is in row and get us only the unrated (unknowns) + + + # Prints: + Getting recommendation for user_id which was not in original model training set + recommendations: New user not in base model so in except block and will foldin the foldin dataset (update the model NOT calculate from scratch) + before updating, M= (3624, 3576) + done updating, M= (6040, 3576) + recommendations: [(1307, 3.6290483094468913), (1394, 3.5741565545425957), (1259, 3.5303836262378048), (1968, 3.4565426585553927), (2791, 3.3470277643217203), (1079, 3.268283171487782), (1198, 3.2381080336246675), (593, 3.204915630088236), (1270, 3.1859618303393233), (2918, 3.1548530640630252)] + + +6. Load previous SVD model and foldin NEW users from file then instantly get recommendations +:: + + format = {'col':1, 'row':0, 'value':2, 'ids': int} + + svd = SVD() + #load base svd model + svd.load_model('SVDModel') + + # load new users by their movie rating data file and use it to fold-in the users into the model (loads data and folds in) + svd.load_updateDataBatch_foldin(filename = 'newUsers.dat', sep='::', format=formate, is_row=True) + + # gets recommendedations + print "recommendations:", svd.recommend(new_userID,is_row=True,only_unknowns=True) + + +- All the normal functionalities of python-recsys are compatible with the incremental update commit. The incremental update can even work if you load the model then foldin a new user or users or even items. + +- Please note that preexisting users can't be folded-in only new users which aren't already in the svd model. Documentation ~~~~~~~~~~~~~ @@ -168,10 +304,8 @@ To create the HTML documentation files from doc/source do: cd doc make html -HTML files are created here: +HTML files are created here: :: doc/build/html/index.html - - diff --git a/recsys/__init__.pyc b/recsys/__init__.pyc deleted file mode 100644 index a6bc57bd3ac78b000f3d61b363cfbd46fbc14d5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 556 zcmY*VO;6k~5S?W|SeCZxPsnjEY@o-gswkD{0R$^?LAhAQ9&KFNF|sE}`*ZmX`~jX5 z1R{xN-kb58%*^!V>ne25_lDk{8T>g;zJ#$v4eW%P3E2Z`4#-ZanUbARGaDjvvU6${ zWEa#NlEt?qdqj3cJKO<2rn~hQl#&HZVw3kdp8-DZrH$Rm82MTKsj>@?o~tolAs$2E z&T^_C5f1qns@rUIs{XPMI#0n*H$SBKf7|Q)9X^_pI^PBD{00@IjB~-F<p@CgT})2e zFfgk{hiKW$3ORn#zSrVnCv(tVoX<DwqLL+yQk@R0lpGGkMgv@QO6eYxB+$~JQcs*R zs=RaW40pLmzYTe`)(H+5NvW?Cmv+I%dKumJ8Fc)W_DHR9k3LMk0<GH^xONXow=UG% zy=ip{+Z6w^kz8MB)3T4L1nN)AuE(9%O1aL7QX6E;h`+Fm-t+GX@UYCq<n#Lq)B%nB diff --git a/recsys/algorithm/__init__.pyc b/recsys/algorithm/__init__.pyc deleted file mode 100644 index 89220e34927d884b6c3dce3b3a58b9984843c127..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 251 zcmYLEO9}!p49(2o7u<RVH`)t`$bc?Y6h+*W+Nsk@JC!zq<83{I7chRnKwe(n3n9s# zy{@YsdkMctsZJ?lpFjZ|5-0$Q1j-Z+DHJJe1{qn{Bya+QxnYOsNl5g;+|}Z1H%OZc z8x^*Q?PlwIxtV=9=fYaf2PDHE;+9&by;IB_gV4sYRV}p$ZWa7lNsKHPQY-7jTVR(O pwXcRj$yi7BE5ep^18T=OH<dxoM>N-_&12__t5uVPczplZOkOKXJ&^za diff --git a/recsys/algorithm/baseclass.pyc b/recsys/algorithm/baseclass.pyc deleted file mode 100644 index 50b4b32a88900c7b87a8c0da46433b026d69f6b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11617 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 diff --git a/recsys/algorithm/factorize.pyc b/recsys/algorithm/factorize.pyc deleted file mode 100644 index cc7a947dcb9f76cedf3a58fec1a2f623f818422f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32828 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 diff --git a/recsys/algorithm/matrix.pyc b/recsys/algorithm/matrix.pyc deleted file mode 100644 index f4df053d8a20bc30468603f38761637e803476ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8411 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 diff --git a/recsys/datamodel/__init__.pyc b/recsys/datamodel/__init__.pyc deleted file mode 100644 index 8d3822860cf95760a3f628de56e2c4b7be24950d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 226 zcmYLDK?(vf4D4D#5j^>cUbG(&5g#B6B8Znz+h8m0R@&}ipX(d^fa$VeNG6kENW#DC zRp>r9&8Fbgk5qX>U?>}AX6(t?oj7L~F(<uLFsi7LxPoCMea3VaO*JeC0F?Itf++h# zzS1ad3@F_rDeHo42W?dfZHh0{CCii27JW=rknf?`*liQ)JY+ewdOJjeUIMtz6@VT2 Qmb7_Ka_{j_zsOYV2d&&UHvj+t diff --git a/recsys/datamodel/data.pyc b/recsys/datamodel/data.pyc deleted file mode 100644 index cae0024d2cec36ea7ac9f0508473dc791dccbe96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12984 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 HjOYFrv)e*0 diff --git a/recsys/evaluation/__init__.pyc b/recsys/evaluation/__init__.pyc deleted file mode 100644 index 0c3acb92feecc6b7ce41ead6f886938ec678ce78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269 zcmYL@U1~xx5QXP@s}`jTa0mCL%?3o;B7G_<{iFCY#3Yz$^CL+V?s8p2H_%BDIxyeN znK=)q{kfU=aQ;+qsg~zGcb+jcz?Rv79)NCP-@=ynD|qdO{G}12Tvwk_lqrTXVR1-C zlPXn?Stc32DePC>pt`4wm-YK<iBHe#hqpfmqI3?i;}`rRf3OrbdLw9;q$Mvly|Oa- zO^RPel`I~mvc|<!58}`l8^difD)(7jhB--hlr@V$q(BAnj*T_*+(&(MX3_ATYqnAI E3qL|aQUCw| diff --git a/recsys/evaluation/baseclass.pyc b/recsys/evaluation/baseclass.pyc deleted file mode 100644 index fa9dc9bb8e2b08d3939d210755c6ca6b9c77ad9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4781 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 Yw>;|_Pr-Y;;k<AYp~b<r<L&r=0va5U;Q#;t diff --git a/recsys/evaluation/prediction.pyc b/recsys/evaluation/prediction.pyc deleted file mode 100644 index 4631a7f19c45d8d8446fa8521b00acfb5fd92c82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3802 zcmdUx>u=md5WvUwJ(5d818LKwmB3IT9a6}tq7qUSDiRX<g<PY((8oz&ZR}0X!H<h~ zui{YhrHK#{U-%Q@Z{RBu%*^^8E$z29P$xULFRy2IW`8^0p9_sN`{Qv4<>TS+mss@A zC?foKAOT1n=sMs~+l9o1)Pt^P+a4qpNUPAT+I9t!8l-jT*0Eg$Rfj_dq8eNPoC7@R z&OlTj_0EiYXCay$^)|-64T$ET)0{_D;=l1}I#@FmQtzZNwl#yTzLJH`v%>bwVUKw5 zZF@WT`kM!v`?j-zojXruGLR<DGec(XZtgs1_7?W(Pf#czYth)Q3z0`#aDnz$xQ<O1 zE?hEKbegQnFZfEs$7ff`%-=lFc``7{-zkc`Xz;+@zAR+wN7Bf<zVywYpC~^>HDsK{ z*^zI0%D-RagDmp*i-GC!%r#$Pn}+t4Hr!<wGsC_r*V;&8ZPt8}_joXLQ5h;^;8$4m z4HO3d9XLe~xNz*ksRQTMn;qVqc9UkCDor2sRUybMLmL|esGtdgCM{_<UL}WIA!C}@ z3oP_wthIW1s#@_uA$xJ!+8c&FS)_YKeyl>HTU#>hsU$C`qtzdpUY>ngs8A1etNc-8 z|FrsrisF!5wca0!8ZLeXizYR|2}xbjmipPq-?4&L6}RBjC0QDbHoLHbwi=gA@1Wap z6OJonep#<l{RbEH9+(=r8k^4?$k5z6?LoO|vlP4zy8g_AAKPFsGQH6fyc{mS{oNfn ztCU|jt-$IT@A+{RxPkkQuVB|$tIu(<lV|YAYHQ|rf>n{#x`w)trdD|lwzj^5rk1f{ zI94c1#u1{G(SA=p#Q=#94^^Qu<Lme*r9V8v_xOoP@|j|fbJ+2DkOx|@pJ{s&d~*SR zgCm|0@VEGdz+gDy1^_(7<D@vu1$o1UO&x1tp7!ww*!5+0WNU{>E=`kX(j?ppfC4ui zWkCLBCG;TWf%vjwi{k1%EP5FQIP=bR_kHJzx8&5E51a-3TXEV=QnEY{nM_p>u>C=h z=FuRbCK(h2Q66HQPER|}6o;*#^(VGm!eOD$&tTD{bgfo%<~>$P>~>xc?|#K~88#x% zjfGz4WFU*T4|z6hiID6orko&G3fjPl*WNtc=-PRaZlnm)?Jukl-EK48ZkKdJER-lI zf3B9e@GR>ri5Mj4f8t(k#JcYZZqKo92F&b;b&YA(Jp|+uv2JQPX5Ge!btT>`>uS@i zdzpgFyx+#mbHU6r_NFu}u}9>yro3MA5!J*j2`nH8WQ;w*c?0Ddf~UMgHKr8@2%TfG zNa9@-6O0Lhhgc$UmBcj~#c~<<BI&e-Wz4RRUtpJCvP%%$#B~xYBvwffA1)tHw3#1a znZ^@E`ku|<Lag#bH_;XreG7$pRYr!n7!JK87b};;EUPF~!b#s-l#AK8yNQ<n*I?Qv z7mzP7?V<~t3f(Xarq8fardp%i@*Jn&u^r>0<PIj%4y+<|C=FxIwrw6lpq;XoPZ=Lh z`jD8MMpA*3Rp#?$9<clPWlbi7v-JNvP81_wkcN}nzTG5}@tvPa(__pb#Boj-_lN7+ zNTUU9uOX7z+<SeJd)pk4*8SJe`hnCcOr+Mr!*Ndd1k;hGPn^QFH+pGuGPYsUuX|*R RrchaQ7F{gf@{Q%2{{X&ENcjK& From e2c0ffedff725114805298c93ae8a511c286f801 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <ibrahim.elseoud@gmail.com> Date: Mon, 14 Aug 2017 15:46:43 +0200 Subject: [PATCH 6/7] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1444df5..a51c658 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ A python library for implementing a recommender system. .. _`Demonstration video is available`: https://youtu.be/tIvQxBfa2d4 --There is also an accompanying `bachelor thesis paper`_ (For those interested) which outlines the background, architecture and discusses the "Folding-in" approach. +- There is also an accompanying `bachelor thesis paper`_ (For those interested) which outlines the background, architecture and discusses the "Folding-in" approach. .. _`bachelor thesis paper`: https://drive.google.com/file/d/0BylQe2cRVWE_RmZoUTJYSGZNaXM/view From 986058d9afd677069a9e52be3b2d270d4b146318 Mon Sep 17 00:00:00 2001 From: Ibrahim Abou Elseoud <ibrahim.elseoud@gmail.com> Date: Mon, 14 Aug 2017 16:33:37 +0200 Subject: [PATCH 7/7] Update README.rst --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a51c658..d09f9a5 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,9 @@ python-recsys A python library for implementing a recommender system. -- Now supports incrementally adding new users or items instead of building the model from scratch for these new users or items via the folding-in technique which was mentioned in Sarwar et al.'s `paper`_ (Titled: Incremental Singular Value Decomposition Algorithms for Highly Scalable Recommender Systems), this latest commit is simply an implementation to it for python-recsys. +Incremental SVD update for python-recsys +======================================== +- python-recsys now supports incrementally adding new users or items instead of building the model from scratch for these new users or items via the folding-in technique which was mentioned in Sarwar et al.'s `paper`_ (Titled: Incremental Singular Value Decomposition Algorithms for Highly Scalable Recommender Systems), this latest commit is simply an implementation to it for python-recsys. .. _`paper`: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.3.7894&rep=rep1&type=pdf @@ -16,7 +18,6 @@ A python library for implementing a recommender system. .. _`bachelor thesis paper`: https://drive.google.com/file/d/0BylQe2cRVWE_RmZoUTJYSGZNaXM/view - Installation ============ @@ -166,7 +167,6 @@ Example (1131, 5.4941438045650068), (2339, 5.4916048051511659)] - Example for incremental update ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Load Movielens dataset and prepare for training and testing: