wiki:POTT/Statistiken

Benutzungsstatistiken zum Projektmanagament-Werkzeug POTT

Um unsere Benutzung des POTTs besser zu verstehen, haben wir einige Statistken erstellt. Folgende Werkzeuge wurden dabei entwickelt:

  • Die regulären Trac-Statistiken, für Teammitglieder einsichtig unter stats. Hier werden Kennzahlen angegeben, wie viel Tickets jeder Benutzer angelegt hat.
  • Eine (öffentliche) Graph-Visualisierung des POTTs unter pott-graph/. In diesem Graph werden die Verbindungen der Tickets und Wiki-Seiten im POTT dargestellt. Er dient vor allem als alternatives Browsing, zeigt aber auch wie wichtig Vernetzung ist.
  • Manuelle Statistiken auf Basis der Ticketreporte 15 und 16. Diese eignen sich zum CSV-Export und zur Datenauswertung mit der Hand (zB Excel oder Matlab).

Diese Seite hier soll vor allem die Erstellung manueller Statistiken erläutern. Vorausgesetzt wird dabei ein wissenschaftliches Linux-Umfeld, gearbeitet wird mit Python/Numpy. Insbesondere soll ein Vergleich zwischen E-Mail-Benutzung und Ticketbenutzung zur Kommunikation erstellt werden. Dazu bedienen wir uns der IMAP-Schnittstelle zum Auslesen von E-Mails (im vorliegenden Beispiel von Gmail).

Diese Wiki-Seite wurde für das POTT-Jubiläum (#1000. Ticket) geschrieben und erläutert die Rechnungen, die in den Blogeintrag Projektmanagament ohne E-Mails? eingeflossen sind.

Trac-Statistiken erzeugen und als CSV herunterladen

Alle Daten vom Trac sind in einer MySQL-Datenbank gespeichert, auf die man z.B. in Form der Reports Zugriff hat. Dort kann man eigene SQL-Queries hinterlegen. So listet 15 etwa alle Ticket mit der Anzahl ihrer Bearbeitungen auf. Der zugehörige SQL-Befehl lautet

SELECT
   C.ticket,
   count(Distinct C.time) AS num_edits,
   T.summary,
   T.component
FROM ticket_change as C, ticket as T
WHERE T.id = C.ticket
GROUP BY C.ticket
ORDER BY num_edits DESC

Ticket #178 ist mit 31 Bearbeitungen derzeit (Okt 2014) führend. Für eine detaillierte Statistik aller Zeiten aller Bearbeitungen hab ich ein JOIN-Query für 16 entworfen:

SELECT DISTINCT U.ticket, U.time AS modified FROM (
   (SELECT T.id AS ticket, T.changetime AS time FROM ticket as T)
   UNION ALL 
   (SELECT C.ticket, C.time FROM ticket_change as C) ORDER BY `ticket` ASC
) as U

Es eignet sich zum direkten Vergleich mit E-Mails, da jede Bearbeitung (im besten Fall jeder Kommentar) einzeln aufgelistet wird. Herunterladen kann man die Reports, in dem man sich das gewünschte Format am Ende der Reportseite auswählt. Zur Verfügung steht insbesondere CSV. Mit zwei wget-Aufrufen sind die obigen Reports heruntergeladen:

#!/bin/bash
# POTT-Daten runterladen
wget -O"pott_num_edits.csv" "https://elearning.physik.uni-frankfurt.de/projekt/report/15?asc=1&format=tab"
wget -O"pott_all_edits.csv" "https://elearning.physik.uni-frankfurt.de/projekt/report/16?asc=1&max=6000&page=1&format=tab"

Die Tabellen sind recht übersichtlich (100kB, weniger als 3000 Zeilen) und mit jeder Tabellenkalkulation weiterverarbeitbar.

Ticket-Statistiken vom POTT

Eine erste Statistik, die über die von Trac gegebenen Statistiken hinausgeht, ist nun etwa die Betrachtung der Verteilung der Anzahl der Kommentare auf ein Ticket. Die benötigten Daten werden von 15 direkt ausgegeben. Obige CSV-Datei kann von NumPy (zB in POKAL) in zwei Zeilen zu einer Liste verarbeitet werden:

#!/usr/bin/python
# expects all those csv files present downloaded by pott generators.

from pylab import *
from scipy import optimize
ion() # interactive plotting on

# Thread-statistiken
A = recfromtxt("pott_num_edits.csv", delimiter="\t", skip_header=1, invalid_raise=False)
num_edits = array([ a[1] for a in A ])

Ein einfaches Histogramm erhält man nun durch Plotting:

subplot(2,1,1)
hist(num_edits, bins=20)
xlabel("Anzahl Ticketantworten")
ylabel("Anzahl Tickets")
title("Histogramm: Wie viel Antworten kriegt ein Ticket?")

subplot(2,1,2)
loglog(num_edits, "o-")
ylabel("Anzahl Ticketantworten")
title("Verteilung der Ticketantworten")

Der generierte Graph ist z.B. auf http://blog.studiumdigitale.uni-frankfurt.de/self/?attachment_id=1078 online:

http://blog.studiumdigitale.uni-frankfurt.de/self/files/2014/10/hist-ticket-antworten-preferential-attachment.png

E-Mail-Statistiken erstellen

Mittels IMAP-Zugriff kann man sich von fast jedem E-Mail-Postfach eine Statistik erstellen, wie viel E-Mails man wann schrieb. Basierend auf den Tutorials imaplib with gmail und extract emails with python via imap schrieb ich diese Zeilen Code, die ein CSV-Tabelle mit E-Mail-Subject und Datum aus einem gewählten IMAP-Ordner erstellen.

Da ich Gmail verwende und alle Physik Online-Emails mit einem Label versehe, habe ich so 3000 Mails seit 2011 extrahiert (Downloadzeit ca drei bis fünf Minuten). Dazu verwende ich folgendes dafür geschriebenes Script:

#!/usr/bin/python
#
# Slurpt Email-Details in CSV-Stdout.
# erwartet IMAP-Passwort auf Stdin.
#
# Public Domain, Sven Koeppel 2014

import sys
import imaplib
import getpass # may be used for password input
import email
import email.header
import datetime

def decode_header(value):
        return ' '.join((item[0].decode(item[1] or 'utf-8').encode('utf-8') for item in email.header.decode_header(value)))

def log(msg):
        # easy python3 compatibility
        print >>sys.stderr, msg

M = imaplib.IMAP4_SSL('imap.example.com')
M.login("mein.useraccount@example.com", getpass.getpass())

rv, mailboxes = M.list()
log(mailboxes)

# Auswahl des Ordners, z.B.:
folder = "Physik eLearning"
# oder
# folder = "BioKemika"

rv, data = M.select('"%s"' % folder) # GMail-spezifisch mit den "Anfuehrungszeichen"

# slurp a lot of mails
rv, data = M.search(None, "ALL")
mail_ids = data[0].split()

log("Slurping %d mails from folder '%s' as CSV output!" % (len(mail_ids), folder))

print "Title \t Date"
for mail_id in mail_ids:
        rv, data = M.fetch(mail_id, '(RFC822)')
        msg = email.message_from_string(data[0][1])
        # subject decode:
        subject = decode_header(msg['Subject'])
        # make sure there is no bad character breaking CSV
        subject = subject.translate(None, "\n\r\t")
        # raw date
        date = msg['Date']

        # write a CSV line
        print "%s\t%s" % (subject, date)

Dieses Script erzeugt auf der Standardausgabe eine tabseparierte CSV-Datei. Aufgerufen kann es zB werden mit

time python download-email-stats.py | tee email-statistics.csv

Vergleich von E-Mail und Ticketaufkommen

Den Vergleich von E-Mail-Aufkommen und Ticketaufkommen hab ich mit Numpy gemacht, was in Anbetracht der heterogenen Daten ziemlich nervig ist. Dafür eignet sich zB Panda von SciPy besser.

#!/usr/bin/python

# expects all those csv files present downloaded by gmail and pott
# generators.

from pylab import *
from datetime import date, datetime, timedelta
import csv
from collections import Counter
from scipy.interpolate import *
ion() # interactive plotting on

# Thread-statistiken
# (die werden in dieser Python-File hier nicht benoetigt)
A = recfromtxt("pott_num_edits.csv", delimiter="\t", skip_header=1, invalid_raise=False)
num_edits = array([ a[1] for a in A ])

# daraus kann man jetzt tolle preferential-attachment-statistiken machen

def parse_pott_time(tstamp):
        # exemplary stamp: 2014-10-09T09:49:15+02:00. Ignore timezone
        return datetime.strptime(tstamp[0:len('2014-10-09T09:49:15')], "%Y-%m-%dT%H:%M:%S")
def parse_imap_time(tstamp):
        # exemplary stamp: Sat, 25 Oct 2014 09:55:34 -0000. Ignore timezone
        from email.utils import mktime_tz, parsedate_tz
        return  datetime.fromtimestamp(mktime_tz(parsedate_tz(tstamp)))
        

# Zeit-Statistiken: POTT-Stats
B = recfromtxt("pott_all_edits.csv", delimiter="\t", skip_header=1)
P = array([ parse_pott_time(b[1]) for b in B ])
# in P sind nun eine Menge Zeitstempel fuer den POTT

# Zeit-Statistiken: GMail-Stats.
# numpy ist zu doof das einzulesen
cr = csv.reader(open('gmail-po-statistics-oct2014.csv', "rb"), delimiter="\t")
C = list(cr)[1:]

def is_pott_mail(csv_line):
        return "POTT" in csv_line[0] # POTT in subject of mail

CnP = filter(lambda m: not is_pott_mail(m), C)
print "%d von %d E-Mails sind POTT-Notifications (%f %%)" % (len(C)-len(CnP), len(C), 1-float(len(CnP))/len(C))

G = array([ parse_imap_time(c[1]) for c in CnP])
# in G sind nun eine Menge Zeitstempel fuer Gmail


# Histogramme erstellen
dataLabels = ["POTT-Tickets", "E-Mails"]
data = [P, G]

# a lot of helper functions to get month/week resolutions:
def week_start_date(year, week):
    d = date(year, 1, 1)    
    delta_days = d.isoweekday() - 1
    delta_weeks = week
    if year == d.isocalendar()[0]:
        delta_weeks -= 1
    delta = timedelta(days=-delta_days, weeks=delta_weeks)
    return d + delta

# round to month by truncating the day of mont
binMonths = lambda d: date(d.year, d.month, 1)
binWeeks = lambda d: week_start_date(d.year, d.isocalendar()[1])
# choose your binning
binable = binMonths
dataCounter = [ Counter(map(binable, datelist)) for datelist in data ]

# make the time axis
timeBins = sorted(set(flatten([ c.keys() for c in dataCounter ])))
dataBins = [[d[k] for k in timeBins] for d in dataCounter]

# make the matplotlib num timeaxis
timeBinNum = map(date2num, timeBins)
timeSpace = linspace(min(timeBinNum), max(timeBinNum), 300)

# plot
fig, ax = subplots(1)
colors = "bg" # rcmy

for label, data, c in zip(dataLabels,dataBins, colors):
        # interpolieren
        f2 = interp1d(timeBinNum, data, kind='cubic')
        plot_date(timeSpace, f2(timeSpace), "-", label=label, color=c)
        plot_date(timeBinNum, data, "o", color=c, clip_on=False)

fig.autofmt_xdate() # rotate according...
ax.set_ylim(0,axis()[-1]) # crop negative values (interpolation)

legend()

Statistiken

Mittelwert und Standardabweichung:

In [19]: mean(dataBins[0]), std(dataBins[0])
Out[19]: (49.709090909090911, 57.602295638367735)

Pearson-Korrelationskoeffizienten berechnen:

In [12]: from scipy.stats import *
In [13]: pearsonr(dataBins[0], dataBins[1])
Out[13]: (0.58377734851545249, 2.8931761616602638e-06)
Last modified 3 years ago Last modified on Mar 6, 2015 4:03:30 PM