Skip to content

Instantly share code, notes, and snippets.

@ishukshin
Created May 19, 2018 01:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ishukshin/a4e1ce6564d22a195c6e98021bdb4b4b to your computer and use it in GitHub Desktop.
Save ishukshin/a4e1ce6564d22a195c6e98021bdb4b4b to your computer and use it in GitHub Desktop.
Making json files concerning Russian presidental elections-2018 using laravel artisan

Generate json files:

php artisan elections:turnouttry 1 php artisan elections:turnouttry 2

Structure: data: {"0.00": {data_per_keys}} keys: {data_per_keys with zeros}

data_per_keys is an object with aggregated values for committees (uik) and voters (voters).

<?php
namespace App\Services;
use Cache;
use Storage;
class ElectionService {
/**
*
* @var array
*/
private $committees = [];
/**
*
* @var array
*/
private $regions = [];
/**
*
* @var integer
*/
private $digits;
/**
*
* @var float
*/
private $step;
/**
*
* @var string
*/
private $directory;
/**
*
* @var array
*/
private $overallStats = [];
/**
*
* @param boolean $prepare prepare committees' data or not
* @param integer $digits decimal digits in percent values
* @param string $directory path relative to /storage/app
*/
public function __construct($prepare = true, $digits = 2, $directory = 'public/elections') {
$this->digits = $digits;
$this->directory = $directory;
$this->step = 1 / pow(10, $this->digits);
if($prepare) {
$this->_prepareCommittees();
}
}
/**
* prepare clean result for a particular turnout
* @return mixed
*/
private function _prepareCleanResult() {
return [
'uik.total' => 0,
'uik.l500' => 0,
'uik.g500l1000' => 0,
'uik.g1000l2000' => 0,
'uik.g2000' => 0,
'voters.total' => 0,
'voters.l500' => 0,
'voters.g500l1000' => 0,
'voters.g1000l2000' => 0,
'voters.g2000' => 0,
];
}
/**
* get the key correspondent to committee size
* @param integer $size
* @return string
*/
public function _getKeyBySize($size) {
return ($size < 500 ? 'l500' : ($size < 1000 ? 'g500l1000' : ($size < 2000 ? 'g1000l2000' : 'g2000')));
}
/**
* get committees which are close to a particular percent
* @param NULL|string $region key like tula
* @param float $aroundPercent
* @return array
*/
private function _getRegionCommittees($region) {
if(!is_null($region)) {
return array_filter($this->committees, function($com) use ($region) {
return ($com['region'] === $region);
});
}
return $this->committees;
}
/**
* building total distribution for committees and voters
* @param NULL|string $region region key like tula
* @return mixed
*/
private function _buildDistribution($region = NULL) {
$this->_prepareEmptyData();
$regionComs = $this->_getRegionCommittees($region);
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step)
{
foreach ($regionComs as $committee) {
$this->_checkCommitteeData($ax, $committee);
}
}
}
/**
* prepare grouped array of committees
* @return array
*/
private function _prepareCommittees() {
$fd = fopen(Storage::path($this->directory . '/edin.csv'), 'r');
$k = 0;
while (($row = fgetcsv($fd, 1000, ",")) !== false) {
# skip first line or empty results (cancelled by CIK etc.)
if (!$k or !isset($row[11])) {
$k++;
continue;
}
# total voters are valid+invalid+lost
$reg = preg_replace("#^(.*)www\.([^\.]+)\.vybory(.*)$#iUs", "\\2", $row[0]);
$regname = trim($row[2]);
# add committee and region
$this->_oneCommitteeToGroup($row, $reg);
if(!isset($this->regions[$reg])) {
$this->regions[$reg] = $regname ?: $reg;
}
}
}
/**
* add committee out of row into particular group
* @param array $row array of a file row
* @param string $reg region key
*/
private function _oneCommitteeToGroup($row, $reg) {
$this->committees[] = [
'region' => $reg,
'voters' => $row[9] + $row[10] + $row[11],
'people' => $row[7],
];
}
/**
* preparing empty stats arrays
* @return void
*/
private function _prepareEmptyData() {
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step)
{
$this->overallStats[$this->_formatTurnout($ax)] = $this->_prepareCleanResult();
}
}
/**
* check if the committee fit the floor-or-ceil condition
* @param float $watchingPercent
* @param mixed $committee
*/
private function _checkCommitteeData($watchingPercent, $committee) {
$key = $this->_getKeyBySize($committee['people']);
$turnout = $this->_formatTurnout($watchingPercent);
$percent = $watchingPercent * $committee['people'] / 100;
if ($committee['voters'] == floor($percent) or $committee['voters'] == ceil($percent)) {
$this->overallStats[$turnout]["uik.$key"]++;
$this->overallStats[$turnout]["voters.$key"] += $committee['voters'];
$this->overallStats[$turnout]["uik.total"]++;
$this->overallStats[$turnout]["voters.total"] += $committee['voters'];
}
}
/**
* store current results into file
* @param string $path
*/
private function _storeJSON($path) {
Storage::put($path, json_encode([
'keys' => $this->_prepareCleanResult(),
'data' => $this->overallStats,
]));
echo(" file $path done at " . date('H:i:s'). ", memory used " . round(memory_get_peak_usage() / 1024 / 1024) ."Mb\n");
}
/**
* building total arrays of turnout tries
* @param boolean $rewriteRegions if we need to rewrite file
* @return mixed
*/
public function buildCountryTurnoutTry($rewriteRegions = false) {
$this->_buildDistribution();
$pref = $this->directory . '/2018-regions';
$this->_storeJSON("$pref-$this->digits-d.json");
foreach(array_keys($this->regions) as $region) {
$this->_buildDistribution($region);
$this->_storeJSON("$pref-$this->digits-d-$region.json");
}
if($rewriteRegions) {
Storage::put("$pref.json", json_encode($this->regions, JSON_UNESCAPED_UNICODE));
}
}
/**
* get average number
* @param array $sizes
* @return float
*/
private function _getAverage($sizes) {
return (array_sum($sizes) / count($sizes));
}
/**
* get approximate width of a exact-number-peak
* @param array $sizes
* @return float in percents
*/
private function _getWidthDelta($sizes) {
return 100 / $this->_getAverage($sizes);
}
/**
* get average size of every group
* @return mixed
*/
private function _getSizes() {
$sizes = [
'l500' => [],
'g500l1000' => [],
'g1000l2000' => [],
'g2000' => [],
];
$totals = [];
foreach($this->committees as $com) {
$key = $this->_getKeyBySize($com['people']);
$sizes[$key][] = $com['people'];
$totals[] = $com['people'];
}
foreach($sizes as $key => $keySizes) {
$sizes[$key] = $this->_getWidthDelta($keySizes);
}
$sizes['total'] = $this->_getWidthDelta($totals);
return $sizes;
}
/**
* get sizes of different size groups
* @return mixed
*/
public function buildSizes() {
$ret = [];
foreach($this->_getSizes() as $key => $size) {
$ret[$key] = round($size, 3);
}
return $ret;
}
/**
* get widths of peaks by size groups
* @return array
*/
public function buildWidths() {
$ret = [];
foreach($this->_getSizes() as $key => $size) {
$ret[$key] = round(100 / $size);
}
return $ret;
}
/**
* get region data out of json
* @param string $region region key like tula
* @return mixed
*/
private function _getRegionData($region) {
$prefix = "$this->directory/2018-regions-$this->digits";
return json_decode(Storage::get("$prefix-d-$region.json"), true);
}
/**
* get results for selection regions only
* @param array $regions
* @return mixed
*/
private function _buildRegionsGrouped($regions) {
$this->_prepareEmptyData();
foreach($regions as $region) {
$data = $this->_getRegionData($region);
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step) {
$this->_addToOverallStats($this->_formatTurnout($ax), $data['data']);
}
}
return ['keys' => $this->_prepareCleanResult(), 'data' => $this->overallStats];
}
private function _addToOverallStats($turnout, $data) {
foreach($data[$turnout] as $keyname => $value) {
$this->overallStats[$turnout][$keyname] += $value;
}
}
/**
* get results for selection regions only
* @param array $regions
* @return mixed
*/
public function getRegionsGrouped($regions) {
return Cache::remember('elections-group-' . join('-', $regions) . '-' . $this->digits, 1440, function() use ($regions){
return $this->_buildRegionsGrouped($regions);
});
}
/**
* formatting percent to exact decimal digits
* @param type $percent
* @return type
*/
private function _formatTurnout($percent) {
return number_format($percent, $this->digits, ".", "");
}
}
<?php
class Kernel extends ConsoleKernel
{
...
protected $commands = [
...
Commands\Elections\TurnoutTry::class,
];
...
}
<?php
namespace App\Console\Commands\Elections;
use Illuminate\Console\Command;
use App\Services\ElectionService;
class TurnoutTry extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'elections:turnouttry {digits}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Make turnout stats';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$d = $this->argument('digits');
$this->info("Started at " . date('H:i:s') . " for $d digits");
$es = new ElectionService(true, $d);
$this->info("Average sizes:" . json_encode($es->buildWidths()));
$this->info("Corresponding widths in percents:" . json_encode($es->buildSizes()));
$es->buildCountryTurnoutTry();
$this->info("Done at " . date('H:i:s'). ", memory used " . round(memory_get_peak_usage() / 1024 / 1024) .'Mb');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment