Research data available for everyone.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. #!/usr/bin/python3 -ttOO
  2. '''
  3. Artificial Intelligence Research Group
  4. University of Lleida
  5. '''
  6. # Libraries
  7. import sys
  8. import math
  9. import ast
  10. import xml.etree.ElementTree as ET
  11. import networkx
  12. import random
  13. import time
  14. import psutil
  15. import pandas
  16. import numpy
  17. import scipy
  18. import reddit_at
  19. # Global functions
  20. def sentiment(sentiment_distribution):
  21. '''
  22. Computes the sentiment from a sentiment distribution of 5 values --> [-2, 2]
  23. '''
  24. sentiment_relevance = [-2, -1, 0, 1, 2]
  25. res = [a * b for a, b in zip(sentiment_relevance, sentiment_distribution)]
  26. return sum(res)
  27. def scale_weight(weight, args):
  28. '''
  29. Scales the weight using a log function
  30. '''
  31. if weight >= 1:
  32. return int(math.floor(math.log(weight, args.log_base)) + 1)
  33. else:
  34. return 0
  35. def get_weighted_color(base_color, min_weight, max_weight, w, hw = 0xCF):
  36. #hw = 0xFF #0xCF # Highest light color to not be completely white
  37. if w >= max_weight:
  38. hw = 0
  39. elif max_weight > min_weight:
  40. hw = int(hw * (float(max_weight - w) / float(max_weight - min_weight)))
  41. color = [a | b for a, b in zip(base_color, [hw, hw, hw])]
  42. contrast = '#FFFFFF' if hw < 0x80 else '#000000'
  43. return contrast, color
  44. def ecai2023_bounded_normal_distribution(mean, minv, maxv ):
  45. '''
  46. Bounded normal distribution, like normal distribution but the two values are always in [min, max]
  47. '''
  48. mu = mean
  49. # sigma = 2 / 3
  50. sigma = 2.0 / (3.0 + (mean * 10.0)) if (mean > 0.0) else 2.0 / (3.0 + (-mean * 10.0))
  51. f = scipy.stats.truncnorm((minv - mu) / sigma, (maxv - mu) / sigma, loc = mu, scale = sigma)
  52. return f.rvs()
  53. def ecai2023_getTNormal(minv, maxv, sigma, mu):
  54. print(" Params for one-sided tnormal: ", minv, maxv, sigma, mu)
  55. return scipy.stats.truncnorm((minv - mu) / sigma, (maxv - mu) / sigma, loc = mu, scale = sigma)
  56. # Classes
  57. class UDebG():
  58. '''
  59. User-based Debate Graph
  60. '''
  61. def __init__(self, DebT, root_id, args):
  62. self.DebT = DebT
  63. self.DebT_root_id = root_id
  64. self.UDebG = networkx.DiGraph()
  65. self.max_weight = None
  66. self.min_weight = None
  67. self.max_pos_edge_weight = None
  68. self.min_pos_edge_weight = None
  69. self.max_neg_edge_weight = None
  70. self.min_neg_edge_weight = None
  71. self.VAF_accepted = None
  72. if args.user == 'wia2021' or not args.input_file:
  73. random.seed(args.seed)
  74. if args.seed is not None:
  75. numpy.random.seed(int(args.seed)) # For scipy
  76. if not args.input_file:
  77. self.ecai2023_gen_UDebG(args)
  78. else:
  79. self.wia2021_set_nodes()
  80. self.wia2021_set_edges()
  81. self.wia2021_UDebG(args)
  82. if args.algorithm.startswith('g'):
  83. if len(args.algorithm) == 2:
  84. self.wia2021_polarized_partition_greedy(args)
  85. elif len(args.algorithm) == 4:
  86. self.ccia2022_polarized_partition_greedy(args)
  87. else:
  88. sys.exit('ERROR: Number of algorithm parameters (%s) not recognized.' % args.algorithm)
  89. else:
  90. sys.exit('ERROR: Algorithm (%s) not recognized.' % args.algorithm)
  91. elif args.user == 'mdai2020':
  92. self.mdai2020_set_nodes()
  93. self.mdai2020_set_edges()
  94. self.mdai2020_draw_DebT(args)
  95. print('MDAI 2020 UDebG...')
  96. self.mdai2020_UDebG()
  97. self.UDebG.remove_node('0') # No root node for the solution
  98. self.mdai2020_skeptical()
  99. self.mdai2020_VAF_valuation(args)
  100. if args.draw_graphs:
  101. self.mdai2020_draw_UDebG(args)
  102. self.mdai2020_UDebG_to_xml(args)
  103. self.VAF_accepted = reddit_at.VAF_solver(args, "%s.udebg.xml" % args.input_file)
  104. if args.draw_graphs:
  105. self.mdai2020_draw_UDebG(args)
  106. def ecai2023_gen_UDebG(self, args):
  107. '''
  108. Generates a random UDebG with specific properties
  109. arg.params comma separated format: N,B
  110. - N: number of nodes (int)
  111. - B: beta parameter for p value (float)
  112. '''
  113. print('ecai2023 Generating random UDebG...')
  114. try:
  115. N, B, DEGCTE = args.params.split(',')
  116. N = int(N)
  117. B = float(B)
  118. DEGCTE = float(DEGCTE)
  119. except:
  120. sys.exit('ERROR: Incorrect parameters for UDebG random generation (%s)' % (args.params))
  121. A = args.alpha # alpha parameter for nodes
  122. if N < 0 or A > 1 or A < 0 or B > 1 or B < 0 or args.log_base < 2 or DEGCTE < 1.0:
  123. sys.exit('ERROR: Incorrect parameter ({}-{}-{}-{}-{})'.format(N, A, B, DEGCTE, args.log_base))
  124. E = int(math.ceil(math.log(N, args.log_base))*DEGCTE) # maximum number of edges
  125. print(' Parameters: nodes = {}, max out edges per node = {}, alpha = {}, beta = {}, DegMultConstant = {}, seed = {}'.format(N, E, A, B, DEGCTE, args.seed))
  126. args.input_file = 'rnd-UDebG-{}-{}-{}-{}-{}-{}'.format(N, E, A, B, args.log_base, args.seed)
  127. self.ecai2023_set_nodes(N, A)
  128. self.ecai2023_set_edges(E, B)
  129. if args.draw_graphs:
  130. self.wia2021_draw_UDebG(args)
  131. if args.scip_output:
  132. self.ccia2022_scip_output(args)
  133. def ecai2023_set_nodes(self, N, A):
  134. '''
  135. Generate N nodes for the random UDebG with opinion weighting scheme in [-A, A]
  136. '''
  137. fl = ecai2023_getTNormal(-A, 0, 1.0 / (1.0 + (A * 20.0)), -A)
  138. fr = ecai2023_getTNormal(0, A, 1.0 / (1.0 + (A * 20.0)), A)
  139. for node_id in range(1, N + 1): # No root node with Id = 0
  140. if (A == 0.0):
  141. ows = random.uniform(-A, A)
  142. else:
  143. ows = fr.rvs() if (random.randint(0, 1)) else fl.rvs()
  144. self.UDebG.add_node('user%i' % node_id, opinion_ws = ows, node_id = node_id)
  145. def ecai2023_set_edges(self, E, B ):
  146. '''
  147. Generate a maximum of E out edges for each node for the random UDebG
  148. Parameter B is ignored
  149. '''
  150. for n1 in self.UDebG.nodes():
  151. list_nodes = list(self.UDebG.nodes())
  152. list_nodes.remove(n1) # No self-answers
  153. for e in range(random.randint(1, E)):
  154. n2 = list_nodes.pop(random.randint(0, len(list_nodes) - 1))
  155. n1_ows = self.UDebG.nodes[n1]['opinion_ws']
  156. n2_ows = self.UDebG.nodes[n2]['opinion_ws']
  157. # when they are in different sides:
  158. if ( (n1_ows * n2_ows < 0.0) or (n1_ows == 0.0 and n2_ows > 0.0) or
  159. (n1_ows > 0.0 and n2_ows == 0.0) ):
  160. dist = -abs(n1_ows - n2_ows) * abs(n1_ows)
  161. else:
  162. # in the same side
  163. dist = 2 * abs(n1_ows) - abs(n1_ows - n2_ows)
  164. #dist = abs(n1_ows - n2_ows) * abs(n1_ows) # Maximum range of [0, 2] for alpha 1
  165. #if n2_ows == 0: # dist must be sign of n1_ows
  166. # dist = dist if n1_ows > 0 else -dist
  167. #else:
  168. # if n1_ows * n2_ows < 0: # Different sign, opposite opinions
  169. # dist = -dist
  170. #if dist > 0: # Max range of dist in [0, 1]
  171. # dist = dist * 2
  172. p = 1 # NOTE: set to 1 (or 0) in order to w = iws, but intended to be random.uniform(0, B)
  173. w = ecai2023_bounded_normal_distribution(dist, -2, 2)
  174. iws = (p, w) # ([0, 1], [-2, 2])
  175. self.UDebG.add_edge(n1, n2, interaction_ws = iws)
  176. def wia2021_set_nodes(self):
  177. '''
  178. UDebG has a node for each user and stores each comment of the user in a list as node data
  179. '''
  180. node_id = 1
  181. for n, nd in self.DebT.nodes(data = True):
  182. if 'title' in nd['data'].attrib: # Root comment
  183. self.UDebG.add_node('0', data = nd, node_id = 0) # Id = 0 for root node (special node)
  184. else: # Regular comment
  185. user = nd['data'].get('author')
  186. if user not in self.UDebG:
  187. self.UDebG.add_node(user, data = [nd], node_id = node_id)
  188. node_id = node_id + 1
  189. else:
  190. self.UDebG.nodes[user]['data'].append(nd)
  191. def wia2021_set_edges(self):
  192. '''
  193. UDebG has an edge between u_a and u_b if there is a reply form u_a to u_b
  194. '''
  195. for e1, e2, ed in self.DebT.edges(data = True):
  196. # u_e1 replies to u_e2
  197. u_e1 = self.DebT.nodes[e1]['data'].get('author')
  198. if 'title' in self.DebT.nodes[e2]['data'].attrib: # Reply to root comment
  199. u_e2 = '0'
  200. else:
  201. u_e2 = self.DebT.nodes[e2]['data'].get('author')
  202. if not self.UDebG.has_edge(u_e1, u_e2):
  203. self.UDebG.add_edge(u_e1, u_e2, data = [ed])
  204. else:
  205. self.UDebG[u_e1][u_e2]['data'].append(ed)
  206. def wia2021_UDebG(self, args):
  207. '''
  208. WIA2021 UDebG. Name of variables following paper notation
  209. '''
  210. print('Generating UDebG for wia2021...')
  211. # Discard auto-replies
  212. for user in self.UDebG.nodes():
  213. if self.UDebG.has_edge(user, user):
  214. self.UDebG.remove_edge(user, user)
  215. # No root node for the solution
  216. self.UDebG.remove_node('0')
  217. # Opinion weighting scheme for nodes (users)
  218. print(' Number of nodes UDebG = {}'.format(self.UDebG.number_of_nodes()))
  219. for n, nd in self.UDebG.nodes(data = True):
  220. s = 0
  221. for cs in nd['data']:
  222. s = s + cs['side']
  223. s = s / len(nd['data'])
  224. nd['opinion_ws'] = s
  225. if s < -1 or s > 1:
  226. sys.exit('ERROR: S value (%f) out of range [-1, 1].' % s)
  227. # Interaction weighting scheme for edges
  228. for e1, e2, ed in self.UDebG.edges(data = True):
  229. p = 0
  230. w = 0
  231. for d in ed['data']:
  232. cu1 = d['data'].find('t').get('id')
  233. W = self.DebT.nodes[cu1]['sentiment_not_normalized']
  234. if W > 0:
  235. p = p + 1 # Counts edges with positive W
  236. w = w + W # Sums all W
  237. p = p / len(ed['data'])
  238. w = w / len(ed['data']) # Aggregation operator (mean)
  239. ed['interaction_ws'] = (p, w)
  240. if p < 0 or p > 1 or w < -2 or w > 2:
  241. sys.exit('ERROR: pair (p, w) (%f, %f) out of range ([0, 1], [-2, 2]).' % (p, w))
  242. if args.draw_graphs:
  243. self.wia2021_draw_UDebG(args)
  244. if args.scip_output:
  245. self.ccia2022_scip_output(args)
  246. sys.exit()
  247. def wia2021_sideness_consistency(self, L, R):
  248. '''
  249. WIA2021 We define the sideness consistency of two sides L and R as:
  250. SC(L, R, G) = LC(L, G) * RC (R, G)
  251. '''
  252. LC = 0
  253. RC = 0
  254. for n in L:
  255. S = self.UDebG.nodes[n]['opinion_ws']
  256. if S <= 0:
  257. LC = LC - S
  258. for n in R:
  259. S = self.UDebG.nodes[n]['opinion_ws']
  260. if S > 0:
  261. RC = RC + S
  262. cardC = len(L) + len(R)
  263. LC = LC / cardC
  264. RC = RC / cardC
  265. SC = LC * RC
  266. if SC < 0 or SC > 0.25:
  267. sys.exit('ERROR: SC (%f) out of range [0, 0.25].' % SC)
  268. return LC, RC, SC
  269. def wia2021_interactions_sentiment(self, L, R):
  270. '''
  271. WIA2021 We define the sentiment of the interactions between users of different sides as follows...
  272. '''
  273. sum_inter = 0
  274. for e1, e2, ed in self.UDebG.edges(data = True):
  275. p = ed['interaction_ws'][0]
  276. w = ed['interaction_ws'][1]
  277. if (e1 in L and e2 in R) or (e1 in R and e2 in L):
  278. sum_inter = sum_inter + (-(2 * ((p - 0.5) ** 2) + 0.5)) * w
  279. SWeight = sum_inter / self.UDebG.number_of_edges() + 2
  280. if SWeight < 0 or SWeight > 4:
  281. sys.exit('ERROR: SWeight (%f) out of range [0, 4].' % SWeight)
  282. return SWeight
  283. def wia2021_BipPol(self, L, R, all_stats = False):
  284. '''
  285. WIA2021 Combine both measures SC and SWeight to define the Bipartition Polarization level of a given partition (L, R)
  286. '''
  287. LC, RC, SC = self.wia2021_sideness_consistency(L, R)
  288. SWeight = self.wia2021_interactions_sentiment(L, R)
  289. BipPol = SC * SWeight
  290. if all_stats:
  291. return LC, RC, SC, SWeight, BipPol
  292. else:
  293. return BipPol
  294. def wia2021_initial_partition(self, args):
  295. '''
  296. WIA2021 Initial partition for polarized partition algorithm
  297. See docstring of wia2021_polarized_partition_greedy for more information
  298. '''
  299. L = []
  300. R = []
  301. if len(args.algorithm) > 1 and args.algorithm[1] == '0': # Random initialization
  302. for n, nd in self.UDebG.nodes(data = True):
  303. if random.random() < 0.5:
  304. L.append(n)
  305. else:
  306. R.append(n)
  307. elif len(args.algorithm) > 1 and args.algorithm[1] == '1': # Negatives and neutral to L and positives to R
  308. for n, nd in self.UDebG.nodes(data = True):
  309. if nd['opinion_ws'] <= 0:
  310. L.append(n)
  311. else:
  312. R.append(n)
  313. elif len(args.algorithm) > 1 and args.algorithm[1] == '2': # L with P = (1 − S(c)) / 2 and in R with 1 − P
  314. for n, nd in self.UDebG.nodes(data = True):
  315. P = (1 - nd['opinion_ws']) / 2
  316. if random.random() < P:
  317. L.append(n)
  318. else:
  319. R.append(n)
  320. else:
  321. sys.exit('ERROR: Algorithm settings (%s) with no partition initialization.' % args.algorithm)
  322. return L, R
  323. def wia2021_find_better_v(self, L, R, LtoR):
  324. '''
  325. WIA2021 Find a node in L such that removing it from L and adding it to R increases BipPol value. If LtoR is false, finds a node in R such that removing it from R and adding it to L increases BipPol value.
  326. '''
  327. L1 = list(L)
  328. R1 = list(R)
  329. BipPol = self.wia2021_BipPol(L, R)
  330. if LtoR: # Search for a node from L to move to R
  331. for n in L:
  332. L1.remove(n)
  333. R1.append(n)
  334. if self.wia2021_BipPol(L1, R1) > BipPol:
  335. return n
  336. L1.append(n)
  337. R1.pop()
  338. else: # Search for a node from R to move to L
  339. for n in R:
  340. R1.remove(n)
  341. L1.append(n)
  342. if self.wia2021_BipPol(L1, R1) > BipPol:
  343. return n
  344. R1.append(n)
  345. L1.pop()
  346. return None
  347. def wia2021_polarized_partition_greedy(self, args):
  348. '''
  349. WIA2021 Polarized Partition greedy algorithm.
  350. Set L contains mainly users in the negative side of the debate and the set R contains mainly user in the positive side of the debate.
  351. Algorithm parameters args.algorithm:
  352. - First char of the string, 'g': Chooses this Greedy Polarized Partition algorithm
  353. - Second char of the string, sets initial partition:
  354. - '0': Distributes uniformly at random in either L or R
  355. - '1': Order vertices by polarity assigning the negatives and neutral to L and the positives to R
  356. - '2': Place user's opinion c randomly in L with probability P = (1 − S(c)) / 2 and in R with probability 1 − P
  357. '''
  358. print('WIA2021 Greedy Bipartite Polarization algorithm...')
  359. L, R = self.wia2021_initial_partition(args)
  360. improving = True
  361. steps = 0
  362. init_BipPol = self.wia2021_BipPol(L, R)
  363. print(" Initial BipPol = %f" % init_BipPol)
  364. while improving and steps < len(self.UDebG):
  365. print('\r Step %i/%i...' % (steps + 1, len(self.UDebG)), end = '')
  366. improving = False
  367. v = self.wia2021_find_better_v(L, R, LtoR = True)
  368. if v:
  369. L.remove(v)
  370. R.append(v)
  371. improving = True
  372. v = self.wia2021_find_better_v(L, R, LtoR = False)
  373. if v:
  374. R.remove(v)
  375. L.append(v)
  376. improving = True
  377. steps = steps + 1
  378. if not improving:
  379. print(' not improving...', end = '')
  380. LC, RC, SC, SWeight, BipPol = self.wia2021_BipPol(L, R, all_stats = True)
  381. print("\n Final BipPol = %f" % BipPol)
  382. if args.draw_graphs:
  383. self.wia2021_draw_UDebG(args, L)
  384. self.wia2021_stats_to_file(args, init_BipPol, steps, LC, RC, SC, SWeight, BipPol, L, R)
  385. def ccia2022_select_neighbor_HC(self, L, R):
  386. '''
  387. CCIA2022 Select Neighbor with Hill Climbing strategy and applies changes: Find a node in L such that removing it from L and adding it to R increases BipPol value, then do the same from R to L.
  388. '''
  389. BipPol = self.wia2021_BipPol(L, R)
  390. changes = 0
  391. i = 0
  392. while i < len(L):
  393. n = L.pop(0)
  394. R.append(n)
  395. bp = self.wia2021_BipPol(L, R)
  396. if bp > BipPol:
  397. BipPol = bp
  398. changes = changes + 1
  399. break
  400. L.append(n)
  401. R.pop()
  402. i = i + 1
  403. i = 0
  404. while i < len(R):
  405. n = R.pop(0)
  406. L.append(n)
  407. bp = self.wia2021_BipPol(L, R)
  408. if bp > BipPol:
  409. changes = changes + 1
  410. break
  411. R.append(n)
  412. L.pop()
  413. i = i + 1
  414. return changes
  415. def ccia2022_select_neighbor_SAHC(self, L, R):
  416. '''
  417. CCIA2022 Select Neighbor with Steepest Ascent Hill Climbing strategy and applies changes: Find a node that changing it from L to R or from R to L increases more the BipPol value. Ties broken randomly.
  418. '''
  419. best_bp = self.wia2021_BipPol(L, R)
  420. best_init_bp = best_bp
  421. imp_nbs = []
  422. i = 0
  423. while i < len(L):
  424. n = L.pop(i)
  425. R.append(n)
  426. bp = self.wia2021_BipPol(L, R)
  427. if bp >= best_bp and bp > best_init_bp:
  428. if bp > best_bp:
  429. best_bp = bp
  430. imp_nbs = []
  431. imp_nbs.append((i, L, R))
  432. L.insert(i, n)
  433. R.pop()
  434. i = i + 1
  435. i = 0
  436. while i < len(R):
  437. n = R.pop(i)
  438. L.append(n)
  439. bp = self.wia2021_BipPol(L, R)
  440. if bp >= best_bp and bp > best_init_bp:
  441. if bp > best_bp:
  442. best_bp = bp
  443. imp_nbs = []
  444. imp_nbs.append((i, R, L))
  445. R.insert(i, n)
  446. L.pop()
  447. i = i + 1
  448. if len(imp_nbs) == 0:
  449. return 0
  450. elif len(imp_nbs) > 0:
  451. i, from_set, to_set = random.choice(imp_nbs)
  452. n = from_set.pop(i)
  453. to_set.append(n)
  454. return 1
  455. def ccia2022_select_neighbor_funtion(self, args):
  456. '''
  457. CCIA2022 Select "select_neighbor" function according to third char of algorithm parameters
  458. '''
  459. if args.algorithm[2] == '0':
  460. return self.ccia2022_select_neighbor_HC
  461. elif args.algorithm[2] == '1':
  462. return self.ccia2022_select_neighbor_SAHC
  463. else:
  464. sys.exit('ERROR: Algorithm parameter for select_neighbor function (%s) not recognized.' % args.algorithm)
  465. def ccia2022_select_restarts(self, args):
  466. '''
  467. CCIA2022 Select number of restarts according to fourth char of algorithm parameters
  468. '''
  469. if args.algorithm[3] == '0':
  470. return 1
  471. elif args.algorithm[3] == '1':
  472. return 10
  473. else:
  474. sys.exit('ERROR: Algorithm parameter for select_restarts function (%s) not recognized.' % args.algorithm)
  475. def ccia2022_randomize_sets(self, L, R, noise):
  476. '''
  477. CCIA2022 Randomize sets by noise percentage (probability to switch set)
  478. '''
  479. R_init_size = len(R)
  480. i = len(L) - 1
  481. while i >= 0:
  482. if random.random() < noise:
  483. n = L.pop(i)
  484. R.append(n)
  485. i = i - 1
  486. i = R_init_size - 1
  487. while i >= 0:
  488. if random.random() < noise:
  489. n = R.pop(i)
  490. L.append(n)
  491. i = i - 1
  492. def ccia2022_polarized_partition_greedy(self, args):
  493. '''
  494. CCIA2022 Polarized Partition greedy algorithm.
  495. Set L contains mainly users in the negative side of the debate and the set R contains mainly user in the positive side of the debate.
  496. Algorithm parameters args.algorithm:
  497. - First char of the string, 'g': Chooses this Greedy Polarized Partition algorithm
  498. - Second char of the string sets initial partition:
  499. - '0': Distributes uniformly at random in either L or R
  500. - '1': Order vertices by polarity assigning the negatives and neutral to L and the positives to R
  501. - '2': Place user's opinion c randomly in L with probability P = (1 − S(c)) / 2 and in R with probability 1 − P
  502. - Third char of the string sets better solution strategy:
  503. - '0': Hill Climbing strategy, pick the first neighbor that improve
  504. - '1': Steepest Ascent Hill Climbing strategy, pick the neighbor that improves more
  505. - Fourth char of the string sets the restarts
  506. - '0': No restarts
  507. - '1': 10 restarts
  508. '''
  509. print('CCIA2022 Greedy Bipartite Polarization algorithm...')
  510. p = psutil.Process()
  511. init_cpu_time = p.cpu_times()[0]
  512. select_neighbor = self.ccia2022_select_neighbor_funtion(args)
  513. L, R = self.wia2021_initial_partition(args)
  514. restarts = self.ccia2022_select_restarts(args)
  515. init_BipPol = self.wia2021_BipPol(L, R)
  516. best_BipPol = init_BipPol
  517. best_L = L[:]
  518. best_R = R[:]
  519. print(" Initial BipPol = %f" % init_BipPol)
  520. while restarts:
  521. changes = 1
  522. steps = 0
  523. while changes and steps < len(self.UDebG):
  524. print('\r Step %i/%i...' % (steps + 1, len(self.UDebG)), end = '')
  525. changes = select_neighbor(L, R)
  526. steps = steps + changes
  527. if not changes:
  528. print(' not improving...')
  529. restarts = restarts - 1
  530. BipPol = self.wia2021_BipPol(L, R)
  531. print(" Try final BipPol = %f" % BipPol)
  532. if best_BipPol < BipPol:
  533. best_BipPol = BipPol
  534. best_L = L[:]
  535. best_R = R[:]
  536. if restarts:
  537. self.ccia2022_randomize_sets(L, R, 0.1)
  538. BipPol = self.wia2021_BipPol(L, R)
  539. print(" Try init BipPol = %f" % BipPol)
  540. LC, RC, SC, SWeight, BipPol = self.wia2021_BipPol(best_L, best_R, all_stats = True)
  541. print(" Final best BipPol = %f" % BipPol)
  542. final_cpu_time = p.cpu_times()[0]
  543. print(" CPU time = %0.3f" % (final_cpu_time - init_cpu_time))
  544. if args.draw_graphs:
  545. self.wia2021_draw_UDebG(args, best_L)
  546. self.wia2021_stats_to_file(args, init_BipPol, steps, LC, RC, SC, SWeight, BipPol, best_L, best_R)
  547. def ccia2022_scip_output(self, args):
  548. '''
  549. UDebG Bipartition problem output format for SCIP solver
  550. '''
  551. print('Writing UDebG Bipartition problem in SCIP format...')
  552. file_vertices = '%s.udebg-vertices.scp' % args.input_file
  553. file_edges = '%s.udebg-edges.scp' % args.input_file
  554. file_stats = '%s.stats' % args.input_file
  555. # Vertices
  556. values = []
  557. with open(file_vertices, 'w') as f:
  558. for n, nd in self.UDebG.nodes(data = True):
  559. f.write('v{} {}\n'.format(self.UDebG.nodes[n]['node_id'], nd['opinion_ws']))
  560. values.append(nd['opinion_ws'])
  561. df = pandas.DataFrame(data = values, columns = ['Node weights'])
  562. stats = str(df.describe()) + '\n'
  563. # Edges
  564. values = []
  565. with open(file_edges, 'w') as f:
  566. for e1, e2, ed in self.UDebG.edges(data = True):
  567. p = ed['interaction_ws'][0]
  568. w = ed['interaction_ws'][1]
  569. cost = (-(2 * ((p - 0.5) ** 2) + 0.5)) * w
  570. f.write('v{} v{} {}\n'.format(self.UDebG.nodes[e1]['node_id'], self.UDebG.nodes[e2]['node_id'], cost))
  571. values.append(cost)
  572. df = pandas.DataFrame(data = values, columns = ['Edge weights'])
  573. stats += str(df.describe()) + '\n'
  574. # Compute stats for the out degree of each node
  575. od = [t[1] for t in self.UDebG.out_degree(self.UDebG.nodes())]
  576. stats += f'Min out degree: {min(od)}\n'
  577. stats += f'Max out degree: {max(od)}\n'
  578. stats += f'Mean out degree: {numpy.mean(od)}\n'
  579. # Write stats to file
  580. with open(file_stats, 'w') as f:
  581. f.write(stats)
  582. def wia2021_stats_to_file(self, args, init_BipPol, steps, LC, RC, SC, SWeight, BipPol, L, R):
  583. '''
  584. Write Greedy polarized partition algorithm statistics and information to file
  585. '''
  586. print('Writing statistics to file...')
  587. out_str = 'WIA2021 stats\n------------\n'
  588. out_str += 'Timestamp = %s\n' % time.ctime()
  589. out_str += 'Input file = %s\n' % args.input_file
  590. out_str += 'UDebG #nodes = %i\n' % self.UDebG.number_of_nodes()
  591. out_str += 'UDebG #edges = %i\n' % self.UDebG.number_of_edges()
  592. out_str += 'UDebG ratio #edges/#nodes = %f\n' % (self.UDebG.number_of_edges() / self.UDebG.number_of_nodes())
  593. out_str += 'Algorithm = %s\n' % args.algorithm
  594. out_str += 'Initial BipPol = %f\n' % init_BipPol
  595. out_str += 'Algorithm steps = %i\n' % steps
  596. out_str += 'Final BipPol = %f\n' % BipPol
  597. out_str += 'LC = %f\n' % LC
  598. out_str += 'RC = %f\n' % RC
  599. out_str += 'SC = %f\n' % SC
  600. out_str += 'SWeight = %f\n' % SWeight
  601. out_str += '#users in L = %i\n' % len(L)
  602. out_str += '#users in R = %i\n' % len(R)
  603. out_str += 'Set L = %s\n' % str([self.UDebG.nodes[n]['node_id'] for n in L])
  604. out_str += 'Set R = %s\n' % str([self.UDebG.nodes[n]['node_id'] for n in R])
  605. # Write to file
  606. output_file_name = '%s.%s.info' % (args.input_file, args.algorithm)
  607. output_file = open(output_file_name, 'w')
  608. output_file.write(out_str)
  609. output_file.close()
  610. def wia2021_draw_UDebG(self, args, L = None):
  611. '''
  612. Drawing WIA2021 UDebG
  613. '''
  614. if L:
  615. print('Drawing wia2021 Bipartite UDebG...')
  616. output_file_name = '%s.udebg.bip.png' % args.input_file
  617. else:
  618. print('Drawing wia2021 UDebG...')
  619. output_file_name = '%s.udebg.png' % args.input_file
  620. gv = networkx.nx_agraph.to_agraph(self.UDebG)
  621. gv.node_attr['style'] = 'filled'
  622. gv.node_attr['fixedsize'] = 'true'
  623. gv.node_attr['width'] = '0.4'
  624. gv.node_attr['height'] = '0.4'
  625. for n in gv.nodes():
  626. node_id = self.UDebG.nodes[n]['node_id']
  627. n.attr['label'] = str(node_id)
  628. bordercolor = '#000000'
  629. n.attr['penwidth'] = 1
  630. if L: # UDebG Bipartite
  631. if n in L:
  632. n.attr['fillcolor'] = '#FF0000'
  633. n.attr['fontcolor'] = '#FFFFFF'
  634. else:
  635. n.attr['fillcolor'] = '#0000FF'
  636. n.attr['fontcolor'] = '#FFFFFF'
  637. else: # UDebG
  638. s = self.UDebG.nodes[n]['opinion_ws']
  639. if s > 0: # cyan = '#4FCFFF'
  640. contrast, color = get_weighted_color([0x4F, 0xCF, 0xFF], 0, 1, s, hw = 0xAF)
  641. contrast = '#000000'
  642. elif s < 0: # dark blue = '#00007F'
  643. contrast, color = get_weighted_color([0x00, 0x00, 0x7F], 0, 1, -s, hw = 0xAF)
  644. else:
  645. color = [0xFF, 0xFF, 0xFF]
  646. contrast = '#000000'
  647. n.attr['fillcolor'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, color)])
  648. n.attr['fontcolor'] = contrast
  649. gv.edge_attr['color'] = '#000000'
  650. for e in gv.edges():
  651. p = self.UDebG[e[0]][e[1]]['interaction_ws'][0]
  652. w = self.UDebG[e[0]][e[1]]['interaction_ws'][1]
  653. if w > 0:
  654. contrast, color = get_weighted_color([0x00, 0xFF, 0x00], 0, 1, p)
  655. elif w < 0:
  656. contrast, color = get_weighted_color([0xFF, 0x00, 0x00], 0, 1, 1 - p)
  657. else:
  658. color = [0x00, 0x00, 0x00]
  659. e.attr['color'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, color)])
  660. gv.layout(prog = 'dot', args='-Goverlap=false -Gnodesep=0.2 -Granksep=0.2 -Grankdir=BT -GK=800 -Gstart=17 -Gmaxiter=600')
  661. gv.draw(output_file_name, format = 'png')
  662. def mdai2020_set_nodes(self):
  663. '''
  664. UDebG has a node for each user and stores each comment of the user in a list as node data
  665. '''
  666. # Set chronological id to comments in DebT
  667. self.DebT.nodes[self.DebT_root_id]['chrono_id'] = 0
  668. id_list = sorted([n for n, nd in self.DebT.nodes(data = True) if 'title' not in nd['data'].attrib])
  669. for i, c_id in enumerate(id_list):
  670. self.DebT.nodes[c_id]['chrono_id'] = i + 1
  671. node_id = 1
  672. for n, nd in self.DebT.nodes(data = True):
  673. if 'title' in nd['data'].attrib: # Root comment
  674. self.UDebG.add_node('0', data = nd['data'], node_id = 0) # Id = 0 for root node (special node)
  675. else: # Regular comment
  676. user = nd['data'].get('author')
  677. if user not in self.UDebG:
  678. self.UDebG.add_node(user, data = [nd['data']], node_id = node_id)
  679. node_id = node_id + 1
  680. else:
  681. self.UDebG.nodes[user]['data'].append(nd['data'])
  682. # Print number of comments per user
  683. # print('User\tId\t#comments')
  684. # for n, nd in self.UDebG.nodes(data = True):
  685. # print('%s\t%i\t%i' % (n, nd['node_id'], len(nd['data'])))
  686. def mdai2020_set_edges(self):
  687. '''
  688. UDebG has an edge between u_a and u_b if there is a reply form u_a to u_b
  689. '''
  690. for e1, e2, ed in self.DebT.edges(data = True):
  691. # u_e1 replies to u_e2
  692. u_e1 = self.DebT.nodes[e1]['data'].get('author')
  693. if 'title' in self.DebT.nodes[e2]['data'].attrib: # Reply to root comment
  694. u_e2 = '0'
  695. else:
  696. u_e2 = self.DebT.nodes[e2]['data'].get('author')
  697. if not self.UDebG.has_edge(u_e1, u_e2):
  698. self.UDebG.add_edge(u_e1, u_e2, data = [ed['data']])
  699. else:
  700. self.UDebG[u_e1][u_e2]['data'].append(ed['data'])
  701. def mdai2020_UDebG(self):
  702. '''
  703. MDAI 2020 requirements: Auto-replies are discarded
  704. '''
  705. for user in self.UDebG.nodes():
  706. if self.UDebG.has_edge(user, user):
  707. self.UDebG.remove_edge(user, user)
  708. def mdai2020_skeptical(self):
  709. '''
  710. Skeptical sentiment weighting scheme L:E --> [-2, 2]
  711. min(sentiment) of replies from u_a to u_b if (all comments of u_a agree with u_b) or (all comments of u_a disagree with u_b)
  712. 0 otherwise
  713. '''
  714. for u1, u2, ed in self.UDebG.edges(data = True):
  715. ed['skeptical'] = None
  716. for answer in ed['data']:
  717. cu1 = answer.find('t').get('id')
  718. s = sentiment(ast.literal_eval(self.DebT.nodes[cu1]['data'].get('sentiment_distribution')))
  719. if ed['skeptical'] == None:
  720. ed['skeptical'] = s
  721. else:
  722. if (s > 0 and ed['skeptical'] > 0) or (s < 0 and ed['skeptical'] < 0):
  723. ed['skeptical'] = min(s, ed['skeptical'])
  724. else:
  725. ed['skeptical'] = 0
  726. break
  727. def mdai2020_VAF_valuation(self, args):
  728. '''
  729. Valuation function for a Valued Argumentation Framework (VAF) over UDebG
  730. '''
  731. if not args.user_valuation:
  732. sys.exit('ERROR: no valuation function selected for the VAF over UDebG.')
  733. for n, nd in self.UDebG.nodes(data = True):
  734. if args.user_valuation == 'comment_karma':
  735. nd['valuation'] = scale_weight(max([int(c.get(args.user_valuation)) for c in nd['data']]), args)
  736. elif args.user_valuation == 'sum_scores':
  737. nd['valuation'] = scale_weight(sum([int(c.get('score')) for c in nd['data']]), args)
  738. vals = [nd['valuation'] for n_id, nd in self.UDebG.nodes(data = True)]
  739. self.max_weight = max(vals)
  740. self.min_weight = min(vals)
  741. pos_vals = [ed['skeptical'] for u1, u2, ed in self.UDebG.edges(data = True) if ed['skeptical'] > 0]
  742. neg_vals = [ed['skeptical'] for u1, u2, ed in self.UDebG.edges(data = True) if ed['skeptical'] < 0]
  743. self.max_pos_edge_weight = max(pos_vals)
  744. self.min_pos_edge_weight = min(pos_vals)
  745. self.max_neg_edge_weight = max(neg_vals)
  746. self.min_neg_edge_weight = min(neg_vals)
  747. def mdai2020_UDebG_to_xml(self, args):
  748. '''
  749. Saves self.UDebG graph to xml file
  750. '''
  751. xml = ET.Element('entailment-corpus')
  752. xml.append(ET.Comment(reddit_at.args2str(args)))
  753. xml.set('num_nodes', str(len(self.UDebG)))
  754. xml.set('num_edges', str(self.UDebG.number_of_edges()))
  755. al_xml = ET.SubElement(xml, 'argument-list')
  756. al_xml.set('minweight', str(self.min_weight))
  757. al_xml.set('maxweight', str(self.max_weight))
  758. for n_id, nd in self.UDebG.nodes(data = True):
  759. a = ET.SubElement(al_xml, 'arg')
  760. a.set('weight', str(nd['valuation']))
  761. a.set('user', str(n_id))
  762. a.set('id', str(nd['node_id']))
  763. ap_xml = ET.SubElement(xml, 'argument-pairs')
  764. for u1, u2, ed in self.UDebG.edges(data = True):
  765. if ed['skeptical'] < 0 and abs(ed['skeptical']) > args.alpha:
  766. p = ET.SubElement(ap_xml, 'pair')
  767. p.set('entailment', 'ATTACKS')
  768. t = ET.SubElement(p, 't')
  769. t.set('id', str(self.UDebG.nodes[u1]['node_id']))
  770. h = ET.SubElement(p, 'h')
  771. h.set('id', str(self.UDebG.nodes[u2]['node_id']))
  772. ET.ElementTree(xml).write("%s.udebg.xml" % args.input_file)
  773. def mdai2020_draw_DebT(self, args):
  774. '''
  775. Drawing Debate Tree
  776. '''
  777. print('Drawing DebT...')
  778. gv = networkx.nx_agraph.to_agraph(self.DebT)
  779. gv.node_attr['style'] = 'filled'
  780. gv.node_attr['fixedsize'] = 'true'
  781. gv.node_attr['width'] = '0.4'
  782. gv.node_attr['height'] = '0.4'
  783. gv.node_attr['fillcolor'] = '#0000FF'
  784. gv.node_attr['fontcolor'] = '#FFFFFF'
  785. for n in gv.nodes():
  786. n.attr['label'] = str(self.DebT.nodes[n]['chrono_id'])
  787. gv.edge_attr['color'] = '#000000'
  788. for e in gv.edges():
  789. s = sentiment(ast.literal_eval(self.DebT.nodes[e[0]]['data'].get('sentiment_distribution')))
  790. if s > 0:
  791. e.attr['color'] = '#00FF00'
  792. elif s < 0:
  793. e.attr['color'] = '#FF0000'
  794. gv.layout(prog = 'dot', args='-Goverlap=false -Gnodesep=0.2 -Granksep=0.2 -Grankdir=BT -GK=800 -Gstart=17 -Gmaxiter=600')
  795. gv.draw("%s.debt.png" % args.input_file, format = 'png')
  796. def mdai2020_draw_UDebG(self, args):
  797. '''
  798. Drawing UDebG
  799. '''
  800. if self.VAF_accepted:
  801. print('Drawing UDebG solution...')
  802. output_file_name = '%s.udebg-sol.png' % args.input_file
  803. else:
  804. print('Drawing UDebG...')
  805. output_file_name = '%s.udebg.png' % args.input_file
  806. gv = networkx.nx_agraph.to_agraph(self.UDebG)
  807. gv.node_attr['style'] = 'filled'
  808. gv.node_attr['fixedsize'] = 'true'
  809. gv.node_attr['width'] = '0.4'
  810. gv.node_attr['height'] = '0.4'
  811. for n in gv.nodes():
  812. node_id = self.UDebG.nodes[n]['node_id']
  813. n.attr['label'] = str(node_id)
  814. bordercolor = [0x00, 0x00, 0x00]
  815. penwidth = 1
  816. fontcolor = '#FFFFFF'
  817. fillcolor = [0x00, 0x00, 0xFF]
  818. if self.VAF_accepted:
  819. fontcolor, fillcolor = get_weighted_color([0x00, 0x00, 0xFF], self.min_weight, self.max_weight, self.UDebG.nodes[n]['valuation'])
  820. if node_id not in self.VAF_accepted:
  821. bordercolor = fillcolor
  822. penwidth = 3
  823. fontcolor, fillcolor = get_weighted_color([0x00, 0x00, 0x00], self.min_weight, self.max_weight, self.UDebG.nodes[n]['valuation'])
  824. n.attr['fontcolor'] = fontcolor
  825. n.attr['fillcolor'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, fillcolor)])
  826. n.attr['color'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, bordercolor)])
  827. n.attr['penwidth'] = penwidth
  828. gv.edge_attr['color'] = '#000000'
  829. for e in gv.edges():
  830. if self.UDebG[e[0]][e[1]]['skeptical'] > 0:
  831. contrast, color = get_weighted_color([0x00, 0xFF, 0x00], self.min_pos_edge_weight, self.max_pos_edge_weight, self.UDebG[e[0]][e[1]]['skeptical'])
  832. e.attr['color'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, color)])
  833. elif self.UDebG[e[0]][e[1]]['skeptical'] < 0:
  834. contrast, color = get_weighted_color([0xFF, 0x00, 0x00], -self.max_neg_edge_weight, -self.min_neg_edge_weight, -self.UDebG[e[0]][e[1]]['skeptical'])
  835. e.attr['color'] = '#%s' % ''.join([c[2:].zfill(2) for c in map(hex, color)])
  836. if self.VAF_accepted:
  837. if abs(self.UDebG[e[0]][e[1]]['skeptical']) > args.alpha:
  838. e.attr['color'] = '#FF0000'
  839. else:
  840. e.attr['color'] = 'transparent' # Like do not draw edge
  841. gv.layout(prog = 'dot', args='-Goverlap=false -Gnodesep=0.2 -Granksep=0.2 -Grankdir=BT -GK=800 -Gstart=17 -Gmaxiter=600')
  842. gv.draw(output_file_name, format = 'png')

Powered by TurnKey Linux.