package tudo.streamingrec.algorithms; import java.util.AbstractMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import tudo.streamingrec.data.ClickData; import tudo.streamingrec.data.Item; import tudo.streamingrec.data.Transaction; import tudo.streamingrec.util.Util; /** * Counts the co-occurrences for two item clicks in one session. * * @author Mozhgan * */ public class FastSessionCoOccurrence extends Algorithm { //a map of co-occurrences between item ids. the key type is string to be more general protected Map<String, Object2IntOpenHashMap<String>> coOcurrenceMap = new Object2ObjectOpenHashMap<>(); //should the whole current sessions be considered or just the current item protected boolean wholeSession = false; //should we only count co-occurrences in the last N clicks? protected boolean buffer = false; //how large should the click buffer be? protected int bufferSize = 10000; //the buffer of co-occurrences from the last N clicks protected LinkedList<AbstractMap.Entry<String, String>> ringBuffer = new LinkedList<>(); @Override protected void trainInternal(List<Item> items, List<ClickData> clickData) { // for all transactions, update the co-occurrence map for (ClickData c : clickData) { updateMap(c.session); } } @Override public LongArrayList recommendInternal(ClickData clickData) { //create a list of scores for each item, which is the sum of all co-occurrence counts Map<String, Double> combineWeights = new Object2DoubleOpenHashMap<String>(); //depending on if we are supposed to use the whole session or not, //this for loop only does one iteration on the last element or it iterates over //all click in the current user sesssion for (int i = wholeSession?0:(clickData.session.size() - 1); i < clickData.session.size(); i++) { Transaction click = clickData.session.get(i); // if the inner map is empty, we cannot recommend anything if (!coOcurrenceMap.containsKey(getCoOccurrenceKey(click))) { continue; } // get the inner map of items that this item has co-occurred with Map<String, Integer> m = coOcurrenceMap.get(getCoOccurrenceKey(click)); for (Entry<String, Integer> entry : m.entrySet()) { // sum up the co-occurrence weights for each item Double currVal = combineWeights.get(entry.getKey()); if(currVal == null){ currVal = 0d; } combineWeights.put(entry.getKey(), currVal + entry.getValue()); } } // sort the weighted sums Map<String, Double> sortedKeys = Util.sortByValue(combineWeights, false); return generateResultList(sortedKeys, clickData); } /** * Generate a result list from a map of summed up co-occurence counts. * In this case, we are just parsing the string back to a long (item id). * In future implementations, this method can be overridden and do more intersting stuff. * @param sortedKeys - * @param clickData - * @return a sorted recommendation list */ protected LongArrayList generateResultList(Map<String, Double> sortedKeys, ClickData clickData) { // remap all item ids back to the actual Item objects LongArrayList sortedItems = new LongArrayList(); for (String itemId : sortedKeys.keySet()) { sortedItems.add(Long.parseLong(itemId)); } return sortedItems; } /** * Updates the co-occurrence map based on the current click of the user * @param session - */ protected void updateMap(List<Transaction> session) { for (int i = 0; i < session.size() - 1; i++) { if (getCoOccurrenceKey(session.get(i)) == getCoOccurrenceKey(session.get(session.size()-1))) { // ignore co-occurrences of items with themselves continue; } // we add one entry to the map with the "correct" order (time-based) addTuple(session.get(i), session.get(session.size()-1)); // map with opposite order addTuple(session.get(session.size()-1), session.get(i)); } //if we are supposed to only look at the last N clicks, //this part of the method checks the buffer and cleans out old clicks. if(buffer){ while(ringBuffer.size()>bufferSize){ //adjust map Entry<String, String> first = ringBuffer.poll(); Object2IntOpenHashMap<String> map = coOcurrenceMap.get(first.getKey()); map.addTo(first.getValue(), -1); map.remove(first.getValue(), 0);//remove if 0 } } } /** * Adds one co-occurrence tuple to the map * * @param a A transaction * @param b Another transaction that the first one occurred with in one session */ private void addTuple(Transaction a, Transaction b) { String keyA = getCoOccurrenceKey(a); String keyB = getCoOccurrenceKey(b); if(buffer){ ringBuffer.add(new AbstractMap.SimpleEntry<String, String>(keyA, keyB)); } // check if the inner map exists if (!coOcurrenceMap.containsKey(keyA)) { coOcurrenceMap.put(keyA, new Object2IntOpenHashMap<String>()); } Object2IntOpenHashMap<String> map = coOcurrenceMap.get(keyA); // check if the item in the inner map exists map.addTo(keyB, 1); } protected String getCoOccurrenceKey(Transaction t) { return "" + t.item.id; } /** * Consider the whole current session or just the current click * @param wholeSession - */ public void setWholeSession(boolean wholeSession) { this.wholeSession = wholeSession; } /** * Should we only look at the N most recent co-occurrences? * @param buffer - */ void setBuffer(boolean buffer) { this.buffer = buffer; } /** * Should we only look at the N most recent co-occurrences? * If so, this method sets how many. * @param bufferSize - */ void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } }