461 lines
19 KiB
Python
461 lines
19 KiB
Python
import copy, re
|
|
from sets import Set
|
|
# NOTE THAT ALL FUNCTIONS HERE WORK BY RETURNING A NEW ARRAY, THERE ARE NO IN-PLACE MODIFICATION METHODS
|
|
# ALSO KEEP IN MIND THAT COLUMNS AND ROW INDICES START FROM ZERO (ie. column A -> 0, E -> 4, Z -> 25, etc)
|
|
# Scans a CSV line, keeping track of separators and quotes
|
|
def scanline(ln,sp=','):
|
|
arr = []
|
|
inq = False
|
|
ind = 0
|
|
buff = ""
|
|
while ind < len(ln):
|
|
if ln[ind] == '"':
|
|
inq = not inq
|
|
ind += 1
|
|
elif ln[ind] == '\\':
|
|
buff += ln[ind+1]
|
|
ind += 2
|
|
elif ln[ind] == sp and not inq:
|
|
arr.append(buff)
|
|
buff = ""
|
|
ind += 1
|
|
else:
|
|
buff += ln[ind]
|
|
ind += 1
|
|
arr.append(buff)
|
|
return arr
|
|
# Load a CSV into a 2D array, automatically filling in unevenly wide rows to make the array square
|
|
def load(f,sp=','):
|
|
array = [scanline(x,sp) for x in open(f,'r').readlines()]
|
|
maxlen = 0
|
|
for i in range(len(array)):
|
|
if len(array[i]) > maxlen: maxlen = len(array[i])
|
|
for i in range(len(array)):
|
|
if len(array[i]) < maxlen: array[i] += [''] * (maxlen - len(array[i]))
|
|
return array
|
|
# Apply a Porter stemmer to every cell in a given range of cols in an array (calling stem with just a list and no cols argument stems _every_ cell)
|
|
# Example outputs: manage, management, manager, managing -> manag; pony, ponies -> poni; reincarnate, reincarnated, reincarnation -> reincarn
|
|
def stem(li,cols=0):
|
|
if cols == 0: cols = range(len(li[0]))
|
|
import porter
|
|
pstemmer = porter.PorterStemmer()
|
|
newlist = copy.deepcopy(li)
|
|
for i in range(len(li)):
|
|
for j in cols:
|
|
string = str(li[i][j])
|
|
for ch in "'"+'"+[]?!\n': string = string.replace(ch,'')
|
|
words = string.split(' ')
|
|
newlist[i][j] = ' '.join([pstemmer.stem(x.strip().lower(),0,len(x.strip())-1) for x in words])
|
|
return newlist
|
|
# Is a string a number?
|
|
def isnumber(s):
|
|
t = re.findall('^-?[0-9,]*\.[0-9,]*$',s)
|
|
return len(t) > 0
|
|
|
|
# Declutters (removes special characters, numerifies numbers) every cell, rules same as those for stem(li,cols=0)
|
|
def declutter(li,cols=0):
|
|
if cols == 0: cols = range(len(li[0]))
|
|
newlist = copy.deepcopy(li)
|
|
for i in range(len(li)):
|
|
for j in cols:
|
|
string = str(li[i][j])
|
|
for ch in "'"+'"+[]?!\n': string = string.replace(ch,'')
|
|
words = string.split(' ')
|
|
newlist[i][j] = ' '.join([x.strip().lower() for x in words])
|
|
if isnumber(newlist[i][j]):
|
|
newlist[i][j] = float(newlist[i][j])
|
|
return newlist
|
|
# Generate a list of individual words occurring in a given column in a given array; useful for generating source lists to do n-grams from
|
|
def wordlist(li,col):
|
|
wlist = []
|
|
for i in range(len(li)):
|
|
words = li[i][col].split(' ')
|
|
for w in words:
|
|
if w not in wlist: wlist.append(w)
|
|
return wlist
|
|
# Generates a list of phrases (complete cell entries)
|
|
def phraselist(li,col):
|
|
wlist = []
|
|
for i in range(len(li)):
|
|
phrase= li[i][col]
|
|
if phrase not in wlist: wlist.append(phrase)
|
|
return wlist
|
|
# Retrieve just a few columns from a given array to make a smaller (narrower) array
|
|
def cols(li,cols):
|
|
result = []
|
|
for i in range(len(li)):
|
|
newline = []
|
|
for c in cols:
|
|
if c >= 0: newline.append(li[i][c])
|
|
else: newline.append(1)
|
|
result.append(newline)
|
|
return result
|
|
# Combine two possibly unsorted arrays matching rows by heading in headingcol1 in li1 and headingcol2 in li2
|
|
# setting linclusive = True makes sure every row in li1 makes it into the output, same with rinclusive and li2
|
|
# Recommended to do some kind of sort after splice is done
|
|
def splice(li1,li2,headingcol1,headingcol2,linclusive=False,rinclusive=False):
|
|
s1 = sorted(li1,key=lambda x:x[headingcol1],reverse=True)
|
|
s2 = sorted(li2,key=lambda x:x[headingcol2],reverse=True)
|
|
l1 = len(s1[0])
|
|
l2 = len(s2[0])
|
|
ind1 = 0
|
|
ind2 = 0
|
|
output = []
|
|
while ind1 < len(s1) and ind2 < len(s2):
|
|
if cmp(s2[ind2][headingcol2],s1[ind1][headingcol1]) == 1:
|
|
if rinclusive: output.append([s2[ind2][headingcol2]] + [''] * (l1-1) + s2[ind2][:headingcol2] + s2[ind2][headingcol2 + 1:])
|
|
ind2 += 1
|
|
elif cmp(s2[ind2][headingcol2],s1[ind1][headingcol1]) == -1:
|
|
if linclusive: output.append([s1[ind1][headingcol1]] + s1[ind1][:headingcol1] + s1[ind1][headingcol1 + 1:] + [''] * (l2-1))
|
|
ind1 += 1
|
|
else:
|
|
output.append([s1[ind1][headingcol1]] + s1[ind1][:headingcol1] + s1[ind1][headingcol1 + 1:] + s2[ind2][:headingcol2] + s2[ind2][headingcol2 + 1:])
|
|
ind1, ind2 = ind1 + 1, ind2 + 1
|
|
while ind1 < len(s1) and linclusive:
|
|
output.append([s1[ind1][headingcol1]] + s1[ind1][:headingcol1] + s1[ind1][headingcol1 + 1:] + [''] * l2)
|
|
ind1 += 1
|
|
while ind2 < len(s2) and rinclusive:
|
|
output.append([s2[ind2][headingcol2]] + [''] * l1 + s2[ind2][:headingcol2] + s2[ind2][headingcol2 + 1:])
|
|
ind2 += 1
|
|
return output
|
|
# Creates a wordlist sorted according to function f taken of an array with the results in the addcols in order
|
|
# eg. sorted_wordlist with addcols = [2,4,6], row is 1 2 4 8 16 32 64, f=lambda x:x[2]+x[1]+1.01*x[0] returns sorting key 84.04
|
|
def sorted_wordlist(li,wcol,addcols,f=lambda x:x[1],rev=True):
|
|
return [x[0] for x in sorted(onegrams(li,wcol,addcols),key=f,reverse=rev)]
|
|
# Utility function, used by twograms, threegrams and fourgrams
|
|
def compose(arg):
|
|
return ' '.join(sorted(list(Set(arg))))
|
|
# Calculate a total sum for every desired column for different exact matches in wcol, column -1 is implied to be 1 for every row
|
|
# for example, consider the array
|
|
# dog 20 3
|
|
# dog house 15 28
|
|
# cat 25 31
|
|
# cat 10 7
|
|
# dog 40 0
|
|
# house 10 14
|
|
# Doing pivot(li,0,[1,-1]) gives you the list:
|
|
# dog 60 2
|
|
# dog house 15 1
|
|
# cat 35 2
|
|
# house 10 1
|
|
# wlist allows you to restrict the table to a given wordlist
|
|
def pivot(li, wcol, addcols,wlist=0,sortkey=lambda x:1):
|
|
if wlist == 0: wlist = phraselist(li,wcol)
|
|
result = {}
|
|
for i in range(len(wlist)):
|
|
result[wlist[i]] = [0] * len(addcols)
|
|
for i in range(len(li)):
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1] * 0.01)
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
if li[i][wcol] in result: result[li[i][wcol]] = [pair[0] + pair[1] for pair in zip(result[li[i][wcol]],nums)]
|
|
array = []
|
|
for word in result.keys():
|
|
array.append([word] + result[word])
|
|
return sorted(array,key=sortkey,reverse=True)
|
|
# Similar to a pivot table but looks at individual keywords. The example list above will return with onegrams(li,0,[1,2]):
|
|
# dog 75 3
|
|
# cat 35 2
|
|
# house 25 2
|
|
def onegrams(li, wcol, addcols,wlist=0,sortkey=lambda x: 1):
|
|
if wlist == 0: wlist = wordlist(li,wcol)
|
|
result = {}
|
|
for i in range(len(wlist)):
|
|
result[wlist[i]] = [0] * len(addcols)
|
|
for i in range(len(li)):
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1] * 0.01)
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
for i in range(len(words)):
|
|
if words[i] in result: result[words[i]] = [pair[0] + pair[1] for pair in zip(result[words[i]],nums)]
|
|
array = []
|
|
for word in result.keys():
|
|
array.append([word] + result[word])
|
|
return sorted(array,key=sortkey,reverse=True)
|
|
# Calculate a total sum for every column in addcols and for every word pair in wcol
|
|
# words do not need to be beside each other or in any particular order, so "buy a dog house", "good house for dog owners", "dog in my house" all go under "dog house"
|
|
def twograms(li,wcol,addcols,wlist=0,sortkey=lambda x:1,allindices=False):
|
|
if wlist == 0: wlist = wordlist(li,wcol)
|
|
result = {}
|
|
if allindices:
|
|
for i in range(len(wlist)):
|
|
for j in range(len(wlist)):
|
|
if i != j: result[compose([wlist[i],wlist[j]])] = [0] * len(addcols)
|
|
for i in range(len(li)):
|
|
if i % int(len(li)/10) == (int(len(li)/10) - 1): print "Two grams: " + str(i) + " / " + str(len(li))
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1]) * 0.01
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
for i in range(len(words)):
|
|
if words[i] in wlist:
|
|
for j in range(i+1,len(words)):
|
|
if words[j] in wlist:
|
|
comb = compose([words[i],words[j]])
|
|
if comb in result: result[comb] = [pair[0] + pair[1] for pair in zip(result[comb],nums)]
|
|
elif allindices == False: result[comb] = nums
|
|
array = []
|
|
for words in result.keys():
|
|
array.append([words] + result[words])
|
|
return sorted(array,key=sortkey,reverse=True)
|
|
# Calculate a total sum for every column in addcols and for every word triplet in wcol (do not need to be beside each other or in any particular order)
|
|
# setting allindices to True slows down the calculation a lot but gives you a CSV with all possible combinations of words, making it convenient for
|
|
# working with the same word list on different data
|
|
def threegrams(li,wcol,addcols,wlist=0,sortkey=lambda x:1,allindices=False):
|
|
if wlist == 0: wlist = wordlist(li,wcol)
|
|
result = {}
|
|
if allindices:
|
|
for i in range(len(wlist)):
|
|
for j in range(len(wlist)):
|
|
for k in range(len(wlist)):
|
|
if i != j and i != k and j != k: result[compose([wlist[i],wlist[j],wlist[k]])] = [0] * len(addcols)
|
|
for i in range(len(li)):
|
|
if i % int(len(li)/10) == (int(len(li)/10) - 1): print "Three grams: " + str(i) + " / " + str(len(li))
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1]) * 0.01
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
for i in range(len(words)):
|
|
if words[i] in wlist:
|
|
for j in range(i+1,len(words)):
|
|
if words[j] in wlist:
|
|
for k in range(j+1,len(words)):
|
|
if words[k] in wlist:
|
|
comb = compose([words[i],words[j],words[k]])
|
|
if comb in result:
|
|
result[comb] = [pair[0] + pair[1] for pair in zip(result[comb],nums)]
|
|
elif allindices == False: result[comb] = nums
|
|
array = []
|
|
for words in result.keys():
|
|
array.append([words] + result[words])
|
|
return sorted(array,key=sortkey,reverse=True)
|
|
# Calculate a total sum for every column in addcols and for every word quadruplet in wcol
|
|
def fourgrams(li,wcol,addcols,wlist=0,sortkey=lambda x:1):
|
|
if wlist == 0: wlist = wordlist(li,wcol)
|
|
result = {}
|
|
for i in range(len(li)):
|
|
if i % int(len(li)/10) == (int(len(li)/10) - 1): print "Four grams: " + str(i) + " / " + str(len(li))
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1]) * 0.01
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
for i in range(len(words)):
|
|
if words[i] in wlist:
|
|
for j in range(i+1,len(words)):
|
|
if words[j] in wlist:
|
|
for k in range(j+1,len(words)):
|
|
if words[j] in wlist:
|
|
for l in range(k+1,len(words)):
|
|
if words[l] in wlist:
|
|
comb = compose([words[i],words[j],words[k],words[l]])
|
|
if comb in result:
|
|
result[comb] = [pair[0] + pair[1] for pair in zip(result[comb],nums)]
|
|
else: result[comb] = nums
|
|
|
|
array = []
|
|
for words in result.keys():
|
|
array.append([words] + result[words])
|
|
return sorted(array,key=sortkey,reverse=True)
|
|
# Filters array, returning only the rows where column wcol of that row contains the query keywords (keywords can appear in any order)
|
|
# This and the other filters are useful for taking a list of entries and creating a list of only valid entries according to some validity characteristic
|
|
# eg:
|
|
# dog house, 15
|
|
# cat, 18
|
|
# dog, 33
|
|
# filter(li,0,'dog'):
|
|
# dog house, 15
|
|
# dog, 33
|
|
def filter(li,wcol,query):
|
|
result = []
|
|
for i in range(len(li)):
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
inlist = True
|
|
queryarray = query.split(' ')
|
|
if queryarray == ['']: queryarray = []
|
|
for w in queryarray:
|
|
if w not in words: inlist = False
|
|
if queryarray == ['*']: inlist = len(li[i][wcol]) > 0
|
|
if inlist: result.append(li[i])
|
|
return result
|
|
# Filters array, requiring column wcol to exactly match query
|
|
def phrasefilter(li,wcol,query):
|
|
result = []
|
|
for i in range(len(li)):
|
|
if li[i][wcol] == query: result.append(li[i])
|
|
return result
|
|
# Filters array, requiring function func taken of the row to return True (or 1)
|
|
def funcfilter(li,func):
|
|
result = []
|
|
for i in range(len(li)):
|
|
if func(li[i]): result.append(li[i])
|
|
return result
|
|
# Adds up columns in addcols for a query matching keyfilter(li,wcol,query); can also be thought of as doing a single n-keyword match
|
|
# eg:
|
|
# dog, 25
|
|
# cat, 15
|
|
# dog, 75
|
|
# dog, 10
|
|
# horse, 55
|
|
# cat, 7
|
|
# search(li,0,[1],'dog') gives ['dog',110]
|
|
def search(li,wcol,addcols,query):
|
|
result = [0] * len(addcols)
|
|
for i in range(len(li)):
|
|
words = [x.strip() for x in li[i][wcol].split(' ')]
|
|
nums = []
|
|
for ac in addcols:
|
|
if ac >= 0:
|
|
num = str(li[i][ac]).replace(',','').replace(' ','')
|
|
if num == '': num = 0
|
|
elif num[-1] == '%': num = float(num[:-1] * 0.01)
|
|
else: num = float(num)
|
|
else: num = 1
|
|
nums.append(num)
|
|
inlist = True
|
|
queryarray = query.split(' ')
|
|
if queryarray == ['']: queryarray = []
|
|
for w in queryarray:
|
|
if w not in words: inlist = False
|
|
if queryarray == ['*']: inlist = len(li[i][wcol]) > 0
|
|
if inlist:
|
|
result = [pair[0] + pair[1] for pair in zip(result,nums)]
|
|
return [query] + result
|
|
# Print a CSV from an array to stdout
|
|
def tochars(array,sp=','):
|
|
string = ""
|
|
for line in array: string += sp.join([str(x) for x in line]) + '\n'
|
|
return string[:-1]
|
|
# Save an array to CSV
|
|
def save(f,array,sp=','):
|
|
writeto = open(f,'w')
|
|
writeto.write(tochars(array,sp))
|
|
writeto.close()
|
|
# Compares keywords by two different parameters from two different lists. For example, li1 can be a list of how much money is spent (on addcol1) on a particular combination of keywords (on keycol1) and li2 can be a list of upgraded accounts with the search query they came from on keycol2, and addcol 2 can be left blank to default to -1 (each row is worth one point). Fourth column is statistical significance.
|
|
# Remember that you may have to filter the list yourself first
|
|
# Arguments:
|
|
# grams = 1 for single keywords, 2 for pairs, 3 for triplets and 4 for quadruplets
|
|
# li1, li2 = your two lists
|
|
# keycol1, keycol2 = where the keywords are located in those two lists
|
|
# addcol1, addcol2 = the columns of what you want to add up, eg. cost (set to -1 or leave blank to make it add 1 for each row)
|
|
# sortkey = function to sort results by (highest first)
|
|
# usestem = stem keywords
|
|
# sigtable = add ratio and significance to table
|
|
# invertratio = set ratio column to col1/col2 instead of col2/col1
|
|
# preformatted = li1 and li2 are already properly formatted
|
|
# justpreformat = convert li1 and li2 into twocolumns for comparison but don't go all the way
|
|
# wordlimit = limit search to some more common keywords for speedup purposes
|
|
# Example: list of customers, some upgraded, with originating keywords, and a list of how much you're paying for each search phrase
|
|
#
|
|
# customers.csv:
|
|
# Name, Keyword, Status
|
|
# Bob Jones, spreadsheet csv software, upgraded
|
|
# Matt Bones, csv python utils, free
|
|
# Army Drones, free spreadsheet, free
|
|
# Glenn Mitt, csv software, upgraded
|
|
# Pat Submitt, python utils software, upgraded
|
|
# Shawn Wit, python spreadsheet program, upgraded
|
|
#
|
|
# costs.csv:
|
|
# csv software, useless, and, irrelevant, data, 5.00, blah, blah
|
|
# python spreadsheet, useless, and, irrelevant, data, 2.50, blah, blah
|
|
# spreadsheet utils, useless, and, irrelevant, data, 10.00, blah, blah
|
|
# csv utils, useless, and, irrelevant, data, 1.50, blah, blah
|
|
#
|
|
# Steps:
|
|
# 1. import spread (if not imported already)
|
|
# 2. upgrades = spread.filter(spread.load('customers.csv'),2,'upgraded')
|
|
# 3. costs = spread.load('costs.csv')
|
|
# 4. res = compare(1,costs,upgrades,0,1,5,invertratio=True)
|
|
# 5. spread.save('saved.csv',res)
|
|
#
|
|
# Res should look like:
|
|
#
|
|
# Keyword, Column 1, Column 2, Ratio, Significance
|
|
# spreadsheet, 12.50, 2, 6.25, -0.389
|
|
# utils, 11.50, 1, 11.50, -0.913
|
|
# csv, 7.50, 2, 3.75, 0.335
|
|
# python, 2.50, 2, 1.25, 2.031
|
|
#
|
|
# Or, if desired, you can:
|
|
# i1,i2 = compare(1,costs,upgrades,0,1,5,justpreformat=True)
|
|
# res1 = compare(1,i1,i2,0,1,5,invertratio=True,preformatted=True)
|
|
# res2 = compare(2,i1,i2,0,1,5,invertratio=True,preformatted=True)
|
|
# res3 = compare(3,i1,i2,0,1,5,invertratio=True,preformatted=True)
|
|
# res4 = compare(4,i1,i2,0,1,5,invertratio=True,preformatted=True)
|
|
#
|
|
# Note that significance is calculated based on col2/col1 regardless of invertratio, since getting 0 upgrades when you should have gotten 2 is not that unlikely, but calculating significance based on col1/col2 would give you infinity as infinity is infinitely far away from 0.5.
|
|
def compare(grams,li1,li2,keycol1,keycol2,addcol1=-1,addcol2=-1,sortkey=lambda x:x[1],usestem=True,sigtable=True,invertratio=False,preformatted=False,justpreformat=False,wordlimit=0):
|
|
gramfuncs = [0,onegrams,twograms,threegrams,fourgrams]
|
|
if preformatted == False:
|
|
s1 = declutter(cols(li1,[keycol1,addcol1]),[1])
|
|
print "Done decluttering/stemming: 1/4"
|
|
s2 = declutter(cols(li2,[keycol2,addcol2]),[1])
|
|
print "Done decluttering/stemming: 2/4"
|
|
s1 = stem(s1,[0]) if usestem else declutter(s1,[0])
|
|
print "Done decluttering/stemming: 3/4"
|
|
s2 = stem(s2,[0]) if usestem else declutter(s2,[0])
|
|
print "Done decluttering/stemming: 4/4"
|
|
else: s1,s2 = li1,li2
|
|
print "Printing sample of list 1"
|
|
print s1[:10]
|
|
print "Printing sample of list 2"
|
|
print s2[:10]
|
|
if justpreformat: return s1,s2
|
|
while type(s1[0][1]) is str: s1.pop(0)
|
|
while type(s2[0][1]) is str: s2.pop(0)
|
|
print "Cleaned invalid rows"
|
|
wl = sorted_wordlist(s1,0,[1])
|
|
if wl.count('') > 0: blank = wl.pop(wl.index(''))
|
|
print "Base wordlist length: " + str(len(wl)) + " ; Top ten: " + str(wl[:10])
|
|
if wordlimit > 0 and wordlimit < len(wl):
|
|
print "Shortening to " + str(wordlimit)
|
|
wl = wl[:wordlimit]
|
|
res1 = gramfuncs[grams](s1,0,[1],wl)
|
|
print "Done search: 1/2"
|
|
res2 = gramfuncs[grams](s2,0,[1],wl)
|
|
print "Done search: 2/2"
|
|
comb = sorted(splice(res1,res2,0,0),key=sortkey,reverse=True)
|
|
if sigtable:
|
|
tot1 = search(s1,0,[1],'')
|
|
tot2 = search(s2,0,[1],'')
|
|
ev = tot2[1]*1.0/tot1[1]
|
|
print "Totals: " + str(tot1[1]) + ", " + str(tot2[1])
|
|
for i in range(len(comb)):
|
|
comb[i].append(comb[i][2 - invertratio]*1.0/(comb[i][1 + invertratio] + 0.000001))
|
|
comb[i].append((comb[i][2] - ev * comb[i][1])*1.0/(ev * comb[i][1] + 0.000001) ** 0.5)
|
|
comb = [['Keyword','Column 1','Column 2','Ratio','Significance']] + comb
|
|
else: comb = [['Keyword','Column 1','Column 2']] + comb
|
|
print "Done"
|
|
return comb
|