Позначки

, ,

Пошук в Інтернеті нашвидкоруч відразу показує що в Україні існують вже декілька клонів західних crowdfunding==спільнокошт сайтів аля Kickstarter, але завдяки популярності Громадського ТБ мабуть найбільш відомим на даний момент є розділ Спільнокошт сайту Biggggidea.com.

Реалізація спільнокошту на Biggggidea.com має дуже корисну особливість – список всіх внесків (дата/час внеску + ім’я чи нікнейм доброчинця + сума, якщо не прихована) доступний для будь-кого, що дає можливість переглянути внески і зібрати статистику. Така відкритість інформації дозволяє кращє контролювати кошти внесків і створює більше довіри до проекту – якщо виключити ймовірність змови адміністрації Biggggidea.com і авторів проекту (що малоймовірно, бо Biggggidea.com і так отримують по 10% від кожного внеску), можна досить чітко судити чи не відкладають автори проекту якусь значну частину пожертв потихому собі в кишеню.

До прикладу, проект освітлення пішоходних переходів у Львові збирає кошти напряму, і звітує про результати збору просто видаючі якісь там цифри в загальний доступ (через spreadsheet на Google Docs). А як взнати чи не пішла половина коштів в кишеню авторам проекту – невідомо. І це в наші непевні часи, коли скрізь одни за одним фіксуються випадки недобросовісних зборів пожертв – що приводить до значного рівня недовіри в народі.

Але перейдемо до статистики – т.я. Громадське ТБ має ще 3 дні до завершення збору аналізувати сьогодні будем Громадське Радіо, яке збирало пожертви на свою діяльність там само. Більше того, аналіз зроблений за допомогою GroovyShell скріпта який теоретично мав би працювати для будь-якого проекту на спільнокошті Biggggidea.com – перевірено на проектах Громадського Радіо та Громадського ТБ. Тому в майбутньому можна збирати статистику пожертв для будь-якого проекту, який фінансується через спільнокошт Biggggidea.com.

Перейдемо власне до статистики… 


Статистика збору коштів для Громадського Радіо

Сторінка проекту на biggggidea.com – http://biggggidea.com/project/323/

Всього внесків: 448
Середній розмір внеску: 152.83 грн
Кількість внесків з прихованою сумою: 78
Середній розмір внеску з прихованою сумою: 221.99 грн (розрахований як різниця між сумою відкритих внесків та загальною сумою вказаною на сторінці проекту, розділена на загальну кількість внесків з прихованою сумою)

Перший внесок: Борис Шавлов, 2013-10-16 09:20:26 = 31 грн
Останній внесок: Коваль Надія, 2013-12-16 23:09:23 = 20 грн

Найбільші внески, грн:
Друзі на Благодійній вечірці [2013-10-21 14:52:00] = 10055
Андрій Осадчук [2013-12-15 18:08:48] = 5005
Андрій Стельмащук [2013-10-25 12:39:08] = 5000
marta dyczok [2013-10-20 20:36:41] = 1500
Павло Різаненко [2013-10-17 18:00:18] = 1000
Максим Стріха [2013-10-23 18:48:57] = 1000
Тарасов Дмитро [2013-10-17 16:28:31] = 1000
Мельніков С.А. [2013-11-07 14:48:40] = 1000
Магазин е-контенту Bookland.com [2013-10-31 13:15:48] = 800
Max Ischenko [2013-12-15 21:03:43] = 800
Андрій Павленко [2013-11-01 15:51:09] = 700

(Доречі, якщо хто не знає – Max Ischenko це лідер проекту Developers.org.ua AKA dou.ua. Айтішники вони скрізь (-: ).

Графіки (клікніть/клацніть по картинці щоб збільшити)

Графік внесків по дням (сума за день, середній розмір за день, кількість окремих внесків за день):

Hromadske Radio stats 1

 

Графік розподілу внесків по розмірам:

Hromadske Radio stats 2

 


 

Код!

Т.я. я є програміст по професії, то звичайно рахував статистику не руками. Для підрахунку статистики був написаний скріпт для GroovyShell з використанням чудової бібліотеки для малювання графіків під назвою JFreeChart (версія 1.0.17).

Для повноти коду потрібні також деякі “макроси” якими я користуюсь в GroovyShell і відповідно тримаю в своєму groovysh.rc – копію якого маю на github.

Сам код в поточному вигляді (без гарного форматування бо це лише скріпт для groovyshell а не повноцінний софт – вибачайте):

//projectId = 392;
projectId = 323;

groovy.grape.Grape.grab(group:'org.codehaus.groovy.modules.http-builder', module:'http-builder')

def httpGet(url, encoding) {
http = new groovyx.net.http.HTTPBuilder(url);
return http.get(["uri":url, contentType: groovyx.net.http.ContentType.BINARY], { resp, inputStream -> return new InputStreamReader(inputStream, encoding).text; })
}

def httpGet(url) {
http = new groovyx.net.http.HTTPBuilder(url);
return http.get(["uri":url, contentType: groovyx.net.http.ContentType.BINARY], { resp, inputStream -> return new InputStreamReader(inputStream, "UTF-8").text; })
}

def nekoParse(String content) {
nekoDomParser = new org.cyberneko.html.parsers.DOMParser();
nekoDomParser.parse(new org.xml.sax.InputSource(new ByteArrayInputStream(content.getBytes("UTF-8"))));
return nekoDomParser.getDocument();
}

def xpathnum(doc, xpathStr) {
xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
return xpath.evaluate(xpathStr, doc, javax.xml.xpath.XPathConstants.NUMBER);
}

def xpathstr(doc, xpathStr) {
xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
return xpath.evaluate(xpathStr, doc, javax.xml.xpath.XPathConstants.STRING);
}

def xpathbool(doc, xpathStr) {
xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
return xpath.evaluate(xpathStr, doc, javax.xml.xpath.XPathConstants.BOOLEAN);
}

def xpathn(doc, xpathStr) {
xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
return xpath.evaluate(xpathStr, doc, javax.xml.xpath.XPathConstants.NODE);
}

def xpathns(doc, xpathStr) {
xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
return xpath.evaluate(xpathStr, doc, javax.xml.xpath.XPathConstants.NODESET);
}

public class Donation {
private final String name;
private final Date date;
private final long sum = 0;
public Donation(final String name, final Date date, final long sum) {
this.name=name;this.date=date;this.sum=sum;
}
public long getSum() { return sum; }
public String getName() { return name; }
public Date getDate() { return date; }

public boolean equals(Object other) {
if(other==null) return false;
if(!other instanceof Donation) return false;
Donation od = (Donation)other;
return (sum==od.sum) && this.name.equals(od.getName()) && this.date.equals(od.getDate());
}

public int hashCode() {
return this.sum*13+this.name.hashCode()+this.date.hashCode();
}
public String toString() {
return "Donation: "+name+" ["+new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)+"] = "+sum;
}
}

def getDonations(page, xpathns, xpathstr) {
dateFormat = new java.text.SimpleDateFormat("d MMMM yyyy 'р.' HH:mm:ss", new Locale("UK"));
donations = new ArrayList();
xpathns(page, "//*[@class='founds']//TR").each{
 name = xpathstr(it, ".//TD[contains(@class, 'founds-td2')]/*[not(@class='date')]").trim();
 date = dateFormat.parse(xpathstr(it, ".//TD[contains(@class, 'founds-td2')]/*[@class='date']").trim());
 sumStr = xpathstr(it, ".//TD[contains(@class, 'founds-td4')]/SPAN/text()").trim();
 sum = 0L;
 if(sumStr.length()>0) {
 sum = Long.parseLong(sumStr);
 }
 donations.add(new Donation(name, date, sum));
}
return donations;
}

dateFormat = new java.text.SimpleDateFormat("d MMMM yyyy 'р.' HH:mm:ss", new Locale("UK"));

projectUrl = "http://biggggidea.com/project/"+projectId+"/spilnokosht/";

if(true){

homePage = nekoParse(httpGet(projectUrl));

totalPages = Integer.parseInt(xpathstr(homePage.documentElement, "//*[@class='pager-list']/A[not(@class='pager-list-next')][last()]").trim())
totalSupporters = Long.parseLong(xpathstr(homePage.documentElement, "//*[@class='support-text'][.//*[@class='counter-text'][contains(text(),'Доброч')]]//*[@class='counter-value']/text()").trim());

donations = new HashSet();

currentPage = totalPages;
while(currentPage>0) {
println "Processing page "+currentPage;
page = nekoParse(httpGet(projectUrl+"?page="+currentPage));

supporters = Long.parseLong(xpathstr(page.documentElement, "//*[@class='support-text'][.//*[@class='counter-text'][contains(text(),'Доброч')]]//*[@class='counter-value']/text()").trim());

if(supporters!=totalSupporters) {
println "Supporters number changed: "+totalSupporters+" => "+supporters;
println "Going 1 page back to account for that."
totalSupporters = supporters;
currentPage++;
continue;
}

donations.addAll(getDonations(page.documentElement, xpathns, xpathstr));

currentPage--;
}

println "Processing done. Donations found: "+donations.size();

homePage = nekoParse(httpGet(projectUrl));

// Get totals
totalMoney = Long.parseLong(xpathstr(homePage.documentElement, "//*[@class='support-text'][.//*[@class='counter-text'][starts-with(text(),'Зібрано з')]]//*[@class='counter-value']/text()").trim());
totalSupporters = Long.parseLong(xpathstr(homePage.documentElement, "//*[@class='support-text'][.//*[@class='counter-text'][contains(text(),'Доброч')]]//*[@class='counter-value']/text()").trim());

//xpathns(homePage.documentElement, "//*[@class='reward']").each{ println xpathstr(it, ".//H2")+" - "+xpathstr(it, ".//SPAN[@class='virtue']"); };

donationsSum=0;
donations.each{ if(it.sum>0){donationsSum+=it.sum;}}
hiddenSum=totalMoney-donationsSum

totalHiddenDonations = donations.grep{ it.sum==0 }.size();
averageHiddenDonation = hiddenSum/totalHiddenDonations;

groups=[
"Hidden" : {it.sum==0},
"0<x<=5" : {it.sum>0 && it.sum<=5},
"5<x<=10" : {it.sum>5 && it.sum<=10},
"10<x<=25" : {it.sum>10 && it.sum<=25},
"25<x<=50" : {it.sum>25 && it.sum<=50},
"50<x<=100" : {it.sum>50 && it.sum<=100},
"100<x<=250" : {it.sum>100 && it.sum<=250},
"250<x<=500" : {it.sum>250 && it.sum<=500},
"500<x<=1000" : {it.sum>500 && it.sum<=1000},
"1000<x<=2000" : {it.sum>1000 && it.sum<=2000},
"2000<x<=3000" : {it.sum>2000 && it.sum<=3000},
"3000<x<=4000" : {it.sum>3000 && it.sum<=4000},
"4000<x<=5000" : {it.sum>4000 && it.sum<=5000},
"5000<x<=10000": {it.sum>5000 && it.sum<=10000},
"10000<x" : {it.sum>10000}
]

dfSkipTime = new java.text.SimpleDateFormat("yyyy-MM-dd");
donationsCalendar=donations.groupBy{dfSkipTime.format(it.date)};

results = new StringBuilder();

results.append("\n\n--=== Project Statistics ===--\n\nTotal donations:").append(donations.size());
results.append("\nAverage donation size:").append(donationsSum/donations.size());
results.append("\nHidden sum donations:").append(totalHiddenDonations);
results.append("\nAverage hidden sum donation:").append(String.valueOf(averageHiddenDonation));

results.append("\n\nDonations by categories:\n[Range]\t[Donations]\n");

groups.each{ results.append(it.key).append("\t").append(String.valueOf(donations.grep(it.value).size())).append("\n"); }

results.append("\nEarliest donation == ").append(donations.min{it.date}.toString());
results.append("\nLatest donation == ").append(donations.max{it.date}.toString());
results.append("\n\nDonations by dates:\n[Date]\t[Donations]\t[Sum]\n");
donationsCalendar.keySet().sort().each{ dd=donationsCalendar.get(it); results.append(it+"\t"+dd.size()+"\t"+dd.sum{it.sum}).append("\n"); }

minTop25Sum=donations.sort{-it.sum}.toList().subList(0,25).min{it.sum}.sum;
results.append("\nTop donators:\n");
donations.grep{it.sum>=minTop25Sum}.sort{-it.sum}.each{ results.append(it).append("\n"); }

println results;
}

////// Chart
new File("/Users/mvmn/_my/_prog/_dev/_lib/jfreechart-1.0.17/lib").listFiles().each{ mv_LD(it); }

def makeChart(dataset, axisname, name) {
chart = org.jfree.chart.ChartFactory.createBarChart(null, axisname, name, dataset, org.jfree.chart.plot.PlotOrientation.HORIZONTAL, false, true, false);
//chart.plot.domainAxis.setCategoryLabelPositions(org.jfree.chart.axis.CategoryLabelPositions.DOWN_90);
chart.plot.renderer.setSeriesItemLabelGenerator(0, new org.jfree.chart.labels.StandardCategoryItemLabelGenerator());
chart.plot.renderer.setSeriesItemLabelsVisible(0, true)
chart.plot.renderer.setItemMargin(0.5);
return new org.jfree.chart.ChartPanel(chart);
}

if(true) {
datasetDBC = new org.jfree.data.category.DefaultCategoryDataset();
groups.each{ datasetDBC.addValue(donations.grep(it.value).size(),"К-сть внесків.",it.key) }

datasetNum = new org.jfree.data.category.DefaultCategoryDataset();
datasetSum = new org.jfree.data.category.DefaultCategoryDataset();
datasetAvg = new org.jfree.data.category.DefaultCategoryDataset();

donationsCalendar.keySet().sort().each{ dd=donationsCalendar.get(it);
datasetNum.addValue(dd.size(), "Donations", it);
datasetSum.addValue(dd.sum{it.sum}, "Sum", it);
datasetAvg.addValue(dd.sum{it.sum}/dd.size(), "Avg.", it); }

chartPanelNum = makeChart(datasetNum, "День", "Кількість внесків (за цей день)");
chartPanelSum = makeChart(datasetSum, "День", "Загальна сума внесків (за цей день), Грн.");
chartPanelAvg = makeChart(datasetAvg, "День", "Середній розмір внеску (за цей день), Грн.");
chartPanelSum.chart.plot.domainAxis.setVisible(false);
chartPanelAvg.chart.plot.domainAxis.setVisible(false);

chartPanelDBC = makeChart(datasetDBC, "Діапазон", "К-сть внесків по розмірам");

frame = new javax.swing.JFrame();
panel = new javax.swing.JPanel();
frame.contentPane.setLayout(new java.awt.BorderLayout());
panel.setLayout(new java.awt.GridLayout(1,3));
panel.add(chartPanelNum);
panel.add(chartPanelSum);
panel.add(chartPanelAvg);
panel.setPreferredSize(new java.awt.Dimension(800, 600));
frame.contentPane.add(new javax.swing.JScrollPane(panel), java.awt.BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);

mv_enframe(chartPanelDBC);
}

println results;
Advertisements