PageRenderTime 52ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Scenario/Experiment.php

http://github.com/jsylvanus/phpScenario
PHP | 517 lines | 249 code | 49 blank | 219 comment | 54 complexity | dad85b9afb7484f68d129581a69918af MD5 | raw file
  1. <?php
  2. /**
  3. * phpScenario
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://www.phpscenario.org/license.php
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@phpscenario.org so we can send you a copy immediately.
  14. *
  15. * @category Scenario
  16. * @package Scenario
  17. * @copyright Copyright (c) 2011-2012 TK Studios. (http://www.tkstudios.com)
  18. * @license http://www.phpscenario.org/license.php New BSD License
  19. */
  20. /**
  21. * Experiment class
  22. *
  23. * Handles experiment-related functions, stores experiment data.
  24. *
  25. * @category Scenario
  26. * @package Scenario
  27. * @copyright Copyright (c) 2011-2012 TK Studios. (http://www.tkstudios.com)
  28. * @license http://www.phpscenario.org/license.php New BSD License
  29. */
  30. class Scenario_Experiment {
  31. /**
  32. * Data analyzer for the experiment, created by calling getResults
  33. *
  34. * @var Scenario_Data_Analyzer
  35. */
  36. protected $_analyzer;
  37. /**
  38. * Name of the experiment.
  39. *
  40. * @var string
  41. */
  42. protected $_experiment_id;
  43. /**
  44. * Row ID for the experiment, usually used by databases.
  45. *
  46. * @var int
  47. */
  48. protected $_row_id;
  49. /**
  50. * Child experiments indexed by name (MultiVar)
  51. *
  52. * @var array
  53. */
  54. protected $_children;
  55. /**
  56. * Parent experiment (means this is a child of a multivar experiment)
  57. *
  58. * @var Scenario_Experiment
  59. */
  60. protected $_parent;
  61. /**
  62. * Array of possible treatments and their weights
  63. *
  64. * @var array
  65. */
  66. protected $_weights;
  67. /**
  68. * String array of possible treatments
  69. *
  70. * @var array
  71. */
  72. protected $_treatments;
  73. /**
  74. * Control name (usually 'default')
  75. *
  76. * @var string
  77. */
  78. protected $_control;
  79. /**
  80. * Construct a new Experiment object.
  81. *
  82. * @param string $experiment_id
  83. * @param int|null $row_id
  84. * @param bool $create
  85. * @param array $options
  86. */
  87. public function __construct($experiment_id, $row_id = null, $create = true, $options = array()) {
  88. if (is_array($options) && array_key_exists('parent', $options)) {
  89. $this->_parent = $options['parent'];
  90. }
  91. if (is_array($options) && array_key_exists('children', $options)) {
  92. $this->_children = $options['children'];
  93. if (count($this->_children) > 0) {
  94. foreach($this->_children as $child) {
  95. $child->setParent($this);
  96. }
  97. }
  98. }
  99. if (is_array($options) && array_key_exists('weightings', $options)) {
  100. $this->_weights = $options['weightings'];
  101. $this->_treatments = array_keys($options['weightings']);
  102. } else if (is_array($options) && array_key_exists('treatments', $options)) {
  103. $this->_treatments = $options['treatments'];
  104. $this->_weights = array();
  105. foreach($this->_treatments as $value) {
  106. $this->_weights[$value] = 50;
  107. }
  108. } else {
  109. $this->_weights = array('default'=>50, 'alternate'=>50);
  110. $this->_treatments = array('default','alternate');
  111. }
  112. $this->_control = (is_array($options) && array_key_exists('control', $options)) ? $options['control'] : 'default';
  113. $this->setExperimentID($experiment_id);
  114. if ($row_id == null) {
  115. $experiment = $this->getCore()->getAdapter()->GetExperiment($experiment_id);
  116. if ($experiment === null && $create) {
  117. $experiment = $this->getCore()->getAdapter()->AddExperiment($experiment_id,
  118. array('weightings'=>$this->_weights, 'control'=>$this->_control),
  119. $this->getParent()
  120. );
  121. }
  122. if ($experiment !== null)
  123. $row_id = $experiment->getRowID();
  124. }
  125. if ($row_id !== null)
  126. $this->_row_id = intval($row_id);
  127. }
  128. /**
  129. *
  130. * @return array data to be serialized
  131. */
  132. public function getData() {
  133. $data = array();
  134. if ($this->isChild()) {
  135. $data['parent'] = $this->_parent->getExperimentID();
  136. }
  137. if ($this->isMultiVar()) {
  138. $data['children'] = array_keys($this->_children);
  139. }
  140. $data['weightings'] = $this->getWeightings();
  141. return $data;
  142. }
  143. /**
  144. * Determine whether this is a child of a multivar experiment (it has a parent)
  145. *
  146. * @return boolean
  147. */
  148. public function isChild() {
  149. return $this->_parent !== null;
  150. }
  151. /**
  152. * Determine whether this is a multivar experiment (parent) or not.
  153. *
  154. * @return boolean
  155. */
  156. public function isMultiVar() {
  157. return $this->_children !== null && $this->_parent === null;
  158. }
  159. /**
  160. * Returns the parent experiment if this is a child of a multivariant experiment.
  161. *
  162. * @return Scenario_Experiment Parent experiment or null.
  163. */
  164. public function getParent() {
  165. return $this->_parent;
  166. }
  167. public function setParent($parent) {
  168. if ($parent instanceof Scenario_Experiment) {
  169. $this->_parent = $parent;
  170. } else if (is_string($parent)) {
  171. $this->_parent = $this->getCore()->getExperiment($parent);
  172. } else {
  173. require_once 'Scenario/Exception.php';
  174. throw new Scenario_Exception('$parent must be a string or instance of Scenario_Experiment');
  175. }
  176. }
  177. /**
  178. * Get child experiments of a multivariant experiment.
  179. *
  180. * @return array
  181. */
  182. public function getChildren() {
  183. return $this->_children;
  184. }
  185. /**
  186. * Get the Row ID associated with this experiment (usually for database use).
  187. *
  188. * @return int The stored Row ID for this record, or null.
  189. */
  190. public function getRowID() {
  191. return $this->_row_id;
  192. }
  193. /**
  194. * Set the Row ID associated with this experiment (usually for database use).
  195. *
  196. * @param int $val
  197. */
  198. public function setRowId($val) {
  199. if ($val === null) {
  200. $this->_row_id = null;
  201. } else if (is_int($val)) {
  202. $this->_row_id = intval($val);
  203. } else {
  204. /**
  205. * @see Scenario_Exception
  206. */
  207. require_once 'Scenario/Exception.php';
  208. throw new Scenario_Exception('$val must be null or an integer value.');
  209. }
  210. }
  211. /**
  212. * Get a treatment for this experiment for the given Identity.
  213. *
  214. * If the treatment does not exist, this method invokes getNewTreatment,
  215. * which may be overridden in subclasses to return treatments according to
  216. * different lists and weights. getTreatment should always return "default"
  217. * among its options.
  218. *
  219. * @param Scenario_Identity $id Identity to use in retrieving a new or existing treatment.
  220. * @param bool $create (Optional) whether or not to create the treatment if it does not exist.
  221. * @return Scenario_Treatment The requested treatment object.
  222. */
  223. public function getTreatment(Scenario_Identity $id, $create = true) {
  224. if ($this->isMultiVar()) {
  225. $multiTreatment = array();
  226. foreach ($this->_children as $child) {
  227. $multiTreatment[$child->getExperimentID()] = $child->getTreatment($id, $create);
  228. }
  229. return $multiTreatment;
  230. }
  231. $adapter = $this->getCore()->getAdapter();
  232. $treatment = $adapter->GetTreatmentForIdentity($this, $id);
  233. if ($treatment == null && $create) { // no treatment was stored in data adapter
  234. // get a new treatment and set it in the adapter
  235. $treatment = $this->getNewTreatment();
  236. $result = $adapter->SetTreatment($this, $treatment, $id);
  237. }
  238. return $treatment;
  239. }
  240. /**
  241. * Set multivariate info.
  242. *
  243. * @param array $variants
  244. */
  245. public function setMultiVars($variants = null) {
  246. if (is_array($variants)) {
  247. $variants = self::FixMultivariateArray($variants);
  248. foreach($variants as $k => $v) {
  249. $this->ensureVariant($k, $v);
  250. }
  251. }
  252. }
  253. /**
  254. * Ensures that a sub-experiment (or variant) is registered with this parent experiment.
  255. *
  256. * If the sub-experiment does not exist, it is created and added to $this->_children.
  257. * Also verifies that weightings match and overwrites them if not.
  258. *
  259. * @param string $k
  260. * @param array $v
  261. * @return null
  262. */
  263. public function ensureVariant($k, $v) {
  264. if (!is_array($this->_children)) $this->_children = array();
  265. // key doesn't exist, so create it
  266. if (!array_key_exists($k, $this->_children)) {
  267. $newExp = new Scenario_Experiment($k, null, true, array(
  268. 'parent' => $this,
  269. 'treatments' => array_merge( array_keys($v['control']), array_keys($v['alternates']) ),
  270. 'weightings' => array_merge( $v['control'], $v['alternates'] )
  271. ));
  272. $this->_children[$k] = $newExp;
  273. return;
  274. }
  275. // it exists already, so make sure it matches
  276. $chk = $this->_children[$k];
  277. $chkWeights = array_merge($v['control'], $v['alternates']);
  278. foreach($chk->getWeightings() as $trt => $w) {
  279. if ($chkWeights[$trt] == $w) continue;
  280. $chk->setWeight($trt, $chkWeights[$trt]);
  281. }
  282. }
  283. /**
  284. * Retrieve the weightings array.
  285. *
  286. * Weightings are stored using their treatment names as keys.
  287. *
  288. * @return array
  289. */
  290. public function getWeightings() {
  291. return $this->_weights;
  292. }
  293. /**
  294. * Set a treatment weight.
  295. *
  296. * @param string $treatment
  297. * @param int $weight
  298. */
  299. public function setWeight($treatment, $weight) {
  300. if (!is_array($this->_treatments)) {
  301. $this->_treatments = array();
  302. }
  303. if (!in_array($treatment, $this->_treatments)) {
  304. $this->_treatments[] = $treatment;
  305. }
  306. if (!is_array($this->_weights)) {
  307. $this->_weights = array();
  308. }
  309. $this->_weights[$treatment] = $weight;
  310. if ($this->getRowID() !== null) {
  311. // update the data entry in DB
  312. }
  313. }
  314. /**
  315. * Takes the array supplied for multivariate settings and expands it.
  316. *
  317. * Expansion is based on the following rules:
  318. *
  319. * 1. An array of strings is expanded to an array of string => true
  320. * 2. A boolean (true OR false) is translated to a simple 50/50 control/alternate test
  321. * 3. A sub-test, if a string array, is treated as a list of alternates
  322. * 4. If weights are not specified (as in #3), they are assumed to be 50
  323. *
  324. * @param array $mv
  325. * @return array
  326. */
  327. public static function FixMultivariateArray($mv) {
  328. if (self::_isStringArray($mv)) {
  329. $theTruth = array();
  330. for($i = 0; $i < count($mv); $i++) {
  331. $theTruth[] = true;
  332. }
  333. $mv = array_combine($mv, $theTruth);
  334. }
  335. foreach ($mv as $k => $var) {
  336. // convert from boolean
  337. if (is_bool($var)) {
  338. $mv[$k] = $var = array('control' => array('default' => 50), 'alternates' => array('alternate' => 50));
  339. continue;
  340. }
  341. if (is_array($var)) {
  342. $keys = array_keys($var);
  343. if (self::_isStringArray($var)) {
  344. // first value in the array is not an array, assume string list
  345. $out = array();
  346. foreach(array_values($var) as $name) {
  347. $out[$name] = 50;
  348. }
  349. $mv[$k] = $var = array('control' => array('default' => 50), 'alternates' => $out);
  350. continue;
  351. }
  352. // at this point we've ruled out bool & string array, so top level structure is assumed correct
  353. if (!array_key_exists('control', $var)) {
  354. // ensure control exists
  355. $mv[$k]['control'] = $var['control'] = array('default' => 50);
  356. }
  357. foreach($var as $k2 => $v2) {
  358. if (self::_isStringArray($v2)) {
  359. $mv[$k][$k2] = $var[$k2] = array_combine($v2, self::_blankWeightArray(count($v2)));
  360. }
  361. }
  362. }
  363. }
  364. return $mv;
  365. }
  366. /**
  367. *
  368. * @param array $arr The array to test
  369. * @return boolean Whether the array contains strings
  370. */
  371. private static function _isStringArray($arr) {
  372. foreach ($arr as $v) {
  373. if (is_string($v))
  374. return true;
  375. }
  376. return false;
  377. }
  378. /**
  379. * Creates an array of default weights (50) to match a key list.
  380. *
  381. * @param int $len
  382. * @return array
  383. */
  384. private static function _blankWeightArray($len) {
  385. $out = array();
  386. for($i = 0; $i < $len; $i++)
  387. $out[] = 50;
  388. return $out;
  389. }
  390. /**
  391. * Retrieves a new Scenario_Treatment object for this experiment.
  392. *
  393. * Should randomize between whatever treatments are desired for the experiment.
  394. * By default, treatments select "default" or "alternate" if supplied with null for their name.
  395. *
  396. * @return Scenario_Treatment
  397. */
  398. public function getNewTreatment() {
  399. require_once 'Scenario/Treatment.php';
  400. $treatment = $this->weightedRandomTreatment();
  401. $treatment_obj = new Scenario_Treatment($treatment, $this);
  402. return $treatment_obj;
  403. }
  404. /**
  405. * Retrieves a treatment name using weighted values.
  406. *
  407. * @return string
  408. */
  409. private function weightedRandomTreatment() {
  410. $weights = $this->getWeightings();
  411. $max = array_sum($weights);
  412. $soFar = 0;
  413. $random = mt_rand(0, $max - 1);
  414. foreach($weights as $k => $v) {
  415. $soFar += $v;
  416. if ($random < $soFar) {
  417. return $k;
  418. }
  419. }
  420. }
  421. /**
  422. * Get the experiment's identifying name.
  423. *
  424. * @return string
  425. */
  426. public function getExperimentID() {
  427. return $this->_experiment_id;
  428. }
  429. /**
  430. * Set the experiment's identifying name.
  431. *
  432. * @param string $experiment_id
  433. */
  434. public function setExperimentID($experiment_id) {
  435. $this->_experiment_id = $experiment_id;
  436. }
  437. /**
  438. * Get the results of this experiment as a Scenario_ResultSet collection.
  439. *
  440. * @return Scenario_ResultSet
  441. */
  442. public function getResults($analysis_only = true) {
  443. return $this->getCore()->getAdapter()->GetResults($this, 0, 100000);
  444. }
  445. public function getAnalysis($analysis_only = true) {
  446. $analyzer = $this->getAnalyzer();
  447. return $analyzer->getResults($analysis_only, $this->getExperimentID());
  448. }
  449. public function getAnalyzer() {
  450. if ($this->_analyzer == null) {
  451. $this->_analyzer = new Scenario_Data_Analyzer($this->getCore(), $this);
  452. }
  453. return $this->_analyzer;
  454. }
  455. /**
  456. * Gets a safe Scenario_Core reference.
  457. *
  458. * @todo Pass the core reference to new experiment objects rather than use the singleton.
  459. * @return Scenario_Core An instance of Scenario_Core appropriate for use in the context of this experiment.
  460. */
  461. public function getCore() {
  462. require_once 'Scenario/Core.php';
  463. return Scenario_Core::getInstance();
  464. }
  465. public function finish(Scenario_Identity $id) {
  466. return $this->getCore()->getAdapter()->FinishExperiment($this, $id);
  467. }
  468. }