Data Science meets Software Data

Markus Harrer, Software Development Analyst

@feststelltaste

JUG Nürnberg, 28.02.2019

Data Science

Was ist Data Science?

"Statistik auf nem Mac."

Nach https://twitter.com/cdixon/status/428914681911070720

Was ist dieses "data"?

"Without data you‘re just another person with an opinion."

W. Edwards Deming

=> Belastbare Erkenntnisse mittels Fakten liefern

Was ist dieses "science"?

"The aim of science is to seek the simplest explanations of complex facts."

Albert Einstein

=> Neue Erkenntnisse verständlich herausarbeiten

Was ist ein Data Scientist?

"Jemand, der mehr Ahnung von Statistik
  hat als ein Softwareentwickler
  und mehr Ahnung von Softwareentwicklung
  als ein Statistiker."

Nach zu https://twitter.com/cdixon/status/428914681911070720

Data Science & Software Data: Perfect match!

Software Data

Was ist Software Data?

  • Statisch
  • Laufzeit
  • Chronologisch
  • Community

=> Krass viel!

Analysen von...

  • Performance-Bottlenecks
  • Verborgene Teamkommunikation
  • Software-Rightsizing
  • Übelste Code-Smells
  • ...

=> vom **Problem** über die Daten zur Erkenntnis!

Grundprinzip (1/2)

I. Idee

II. Laden

III. Filtern

Grundprinzip (2/2)

IV. Joinen

V. Aggregieren // fun part

VI. Visualisieren // not so funny part, but...

Technologie (1/2)

Eher zweiranging...jetzt hier vorwiegend:

  • Jupyter (mit RISE)
  • Python 3
  • pandas
  • matplotlib
  • ...

Technologie (2/2)

Es geht aber auch...

  • Jupyter // :-)
  • Java Groovy / beakerx / tablesaw
  • Cypher / Neo4j mit jQAssistant
  • ...

Praktischer Teil

Erstes Hands-On!

I. Idee (1/2)

Aufgabe

Analyse einer Java-Web-Anwendung anhand von Daten aus der Git-Versionskontrolle.

  1. Gibt es Module mit Teammonopole?
  2. Wie gut passt die Modularisierung zum Team?
  3. Wie sieht eine alternative Modularisierung aus?

I. Idee (2/2)

Umsetzung

  • Datenquelle: Git & Excel-Sheet
  • Werkzeuge: Jupyter, pandas, Visualisierungen, Low-End Machine Learning (MDS, AHC)

Meta-Ziel: Alles mal sehen anhand eines einfachen Show-Cases.

Frage 1: Gibt es Module mit Teammonopole?

Unsere Heurisik: **Ändert** nur **ein Team** hauptsächlich Module?

II. Laden

  • Mögliche Datenquelle: Git
  • Helferlein: Open Zippy Analysis Platform For Data In Software

In [1]:
from ozapfdis import git
log = git.log_numstat("../../../dropover/")
log.head(3)


Out[1]:
additions deletions file sha timestamp author
0 191 0 backend/pom-2016-07-16_04-40-56-752.xml 8c686954 2016-07-22 17:43:38 Michael
1 1 1 backend/src/test/java/at/dropover/scheduling/i... 97c6ef96 2016-07-16 09:51:15 Markus
2 19 3 backend/src/main/webapp/app/widgets/gallery/js... 3f7cf92c 2016-07-16 09:07:31 Markus

Was haben wir hier eigentlich?


In [2]:
log.info()


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2403 entries, 0 to 2402
Data columns (total 6 columns):
additions    2403 non-null object
deletions    2403 non-null object
file         2403 non-null object
sha          2403 non-null object
timestamp    2403 non-null datetime64[ns]
author       2403 non-null object
dtypes: datetime64[ns](1), object(5)
memory usage: 112.7+ KB

1 DataFrame (~ programmierbares Excel-Arbeitsblatt), 6 Series (= Spalten), 2403 Rows (= Einträge)

III. Filtern

  • Daten sind oft zu viel und durchwachsen
  • Filtern hilft beim eingrenzen

Wir machen nur mit Java-Produktionscode weiter


In [3]:
java = log.copy()
java = java[java['file'].str.startswith("backend/src/main/java")]
java = java[~java['file'].str.contains("package-info.java")]
java.head()


Out[3]:
additions deletions file sha timestamp author
4 3 4 backend/src/main/java/at/dropover/files/intera... ec85fe73 2016-07-16 08:12:29 Chris
36 3 2 backend/src/main/java/at/dropover/scheduling/i... bfea33b8 2016-07-16 02:02:02 Markus
44 23 1 backend/src/main/java/at/dropover/scheduling/i... ab9ad48e 2016-07-16 00:50:20 Chris
47 68 6 backend/src/main/java/at/dropover/files/intera... 0732e9cb 2016-07-16 00:27:20 Chris
53 7 3 backend/src/main/java/at/dropover/framework/co... ba1fd215 2016-07-15 22:51:46 Chris

IV. Joinen

  • Oft müssen Daten zusätzlich mit verschnitten werden
  • Mehr Perspektiven auf ein Problem möglich

Wir ordnen bei uns Committer zu Teams zu.

Schritt 1: Weitere Datenquelle einlesen.


In [4]:
import pandas as pd
orga = pd.read_excel("../dataset/Teamorganisation.xlsx", index_col=0)
orga


Out[4]:
team
name
Markus A
Chris B
Michael C

Wir ordnen bei uns Committer zu Teams zu.

Schritt 2: Datenquellen joinen


In [5]:
java = java.join(orga, on='author')
java.head()


Out[5]:
additions deletions file sha timestamp author team
4 3 4 backend/src/main/java/at/dropover/files/intera... ec85fe73 2016-07-16 08:12:29 Chris B
36 3 2 backend/src/main/java/at/dropover/scheduling/i... bfea33b8 2016-07-16 02:02:02 Markus A
44 23 1 backend/src/main/java/at/dropover/scheduling/i... ab9ad48e 2016-07-16 00:50:20 Chris B
47 68 6 backend/src/main/java/at/dropover/files/intera... 0732e9cb 2016-07-16 00:27:20 Chris B
53 7 3 backend/src/main/java/at/dropover/framework/co... ba1fd215 2016-07-15 22:51:46 Chris B

V. Aggregieren

  • Vorhandene Daten sind oft zu viel für manuelle Sichtung
  • Neue Einsichten über Problem aber oft auf hoher Flugbahn möglich
  • Daher feingranulare Daten sinnvoll zusammenfassen

Wir fassen Daten nach Modulen (=Bestandteil vom Package-Name) zusammen.


In [6]:
java['module'] = java['file'].str.split("/").str[6]
java.head()


Out[6]:
additions deletions file sha timestamp author team module
4 3 4 backend/src/main/java/at/dropover/files/intera... ec85fe73 2016-07-16 08:12:29 Chris B files
36 3 2 backend/src/main/java/at/dropover/scheduling/i... bfea33b8 2016-07-16 02:02:02 Markus A scheduling
44 23 1 backend/src/main/java/at/dropover/scheduling/i... ab9ad48e 2016-07-16 00:50:20 Chris B scheduling
47 68 6 backend/src/main/java/at/dropover/files/intera... 0732e9cb 2016-07-16 00:27:20 Chris B files
53 7 3 backend/src/main/java/at/dropover/framework/co... ba1fd215 2016-07-15 22:51:46 Chris B framework

Wir markieren Dateiänderungen über neues Flag.


In [7]:
java['changed'] = 1
java.head()


Out[7]:
additions deletions file sha timestamp author team module changed
4 3 4 backend/src/main/java/at/dropover/files/intera... ec85fe73 2016-07-16 08:12:29 Chris B files 1
36 3 2 backend/src/main/java/at/dropover/scheduling/i... bfea33b8 2016-07-16 02:02:02 Markus A scheduling 1
44 23 1 backend/src/main/java/at/dropover/scheduling/i... ab9ad48e 2016-07-16 00:50:20 Chris B scheduling 1
47 68 6 backend/src/main/java/at/dropover/files/intera... 0732e9cb 2016-07-16 00:27:20 Chris B files 1
53 7 3 backend/src/main/java/at/dropover/framework/co... ba1fd215 2016-07-15 22:51:46 Chris B framework 1

Wir fassen Änderungen der Klassen pro Komponenten und Team zusammen.


In [8]:
changes = java.groupby(['module', 'team'])[['changed']].sum()
changes.head()


Out[8]:
changed
module team
comment A 41
B 76
C 1
creator A 14
B 33

Wir berechnen alle erfolgten Änderungen pro Modul...


In [9]:
changes['all'] = changes.groupby('module').transform('sum')
changes.head()


Out[9]:
changed all
module team
comment A 41 118
B 76 118
C 1 118
creator A 14 54
B 33 54

...und damit die Änderungsverhältnisse pro Team.


In [10]:
changes['ratio'] = changes['changed'] / changes['all']
changes.head()


Out[10]:
changed all ratio
module team
comment A 41 118 0.347458
B 76 118 0.644068
C 1 118 0.008475
creator A 14 54 0.259259
B 33 54 0.611111

IV. Visualisieren

  • Grafische Darstellung geben Analysen den letzten Schliff
  • Probleme können Außenstehenden visuell dargestellt besser kommuniziert werden

Wir bauen uns ein Balkendiagramm mit den Verhältnissen der Teamänderungen.


In [11]:
changes['ratio'].unstack().plot.bar(stacked=True);


Fortgeschrittenes Click-Through!

Frage 2: Wie gut passt die Modularisierung zum Team?

Unsere Heuristik: Werden fachliche Komponenten **zusammengehörig geändert**?

Wir analysieren für alle Dateien alle Änderungen (~ Stempelkarte).


In [12]:
commit_matrix = java.pivot('file', 'sha', 'changed').fillna(0)
commit_matrix.iloc[:5,50:55]


Out[12]:
sha 3597d8a2 3b70ea7e 3d3be4ca 3e4ae692 429b3b32
file
backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java 0.0 0.0 0.0 0.0 0.0
backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java 0.0 0.0 0.0 1.0 0.0
backend/src/main/java/at/dropover/comment/boundary/CommentData.java 0.0 0.0 0.0 1.0 0.0
backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java 0.0 0.0 0.0 0.0 0.0
backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java 0.0 0.0 0.0 0.0 0.0

Wir berechnen den Abstand zwischen den vorgenommmenen Commits pro Datei (=Vektor)...


In [13]:
from sklearn.metrics.pairwise import cosine_distances

dis_matrix = cosine_distances(commit_matrix)
dis_matrix[:5,:5]


Out[13]:
array([[0.        , 0.29289322, 0.5       , 0.18350342, 0.29289322],
       [0.29289322, 0.        , 0.29289322, 0.1339746 , 0.5       ],
       [0.5       , 0.29289322, 0.        , 0.59175171, 0.29289322],
       [0.18350342, 0.1339746 , 0.59175171, 0.        , 0.42264973],
       [0.29289322, 0.5       , 0.29289322, 0.42264973, 0.        ]])

...und machen das schöner...


In [14]:
dis_df = pd.DataFrame(
    dis_matrix,
    commit_matrix.index,
    commit_matrix.index)
dis_df.iloc[:5,:2]


Out[14]:
file backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java
file
backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java 0.000000 0.292893
backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java 0.292893 0.000000
backend/src/main/java/at/dropover/comment/boundary/CommentData.java 0.500000 0.292893
backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java 0.183503 0.133975
backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java 0.292893 0.500000

...und visualisieren das Zwischenergebnis.


In [15]:
import seaborn
ax = seaborn.heatmap(dis_df, xticklabels=False, yticklabels=False);


Weiter: Wir brechen nun die mehrdimensionale Matrix auf eine (fast gleichwertige) 2D-Repräsentation...


In [16]:
from sklearn.manifold import MDS

model = MDS(dissimilarity='precomputed', random_state=0)
dis_2d = model.fit_transform(dis_df)
dis_2d[:5]


Out[16]:
array([[-0.5259277 ,  0.45070158],
       [-0.56826041,  0.21528001],
       [-0.52746829,  0.34756761],
       [-0.55856713,  0.26202797],
       [-0.4036568 ,  0.49803657]])

...und machen das mal wieder schöner...


In [17]:
dis_2d_df = pd.DataFrame(
    dis_2d,
    commit_matrix.index,
    ["x", "y"])
dis_2d_df.head()


Out[17]:
x y
file
backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java -0.525928 0.450702
backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java -0.568260 0.215280
backend/src/main/java/at/dropover/comment/boundary/CommentData.java -0.527468 0.347568
backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java -0.558567 0.262028
backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java -0.403657 0.498037

...inkl. der Module.


In [18]:
dis_2d_df['module'] = dis_2d_df.index.str.split("/").str[6]
dis_2d_df.head()


Out[18]:
x y module
file
backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java -0.525928 0.450702 comment
backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java -0.568260 0.215280 comment
backend/src/main/java/at/dropover/comment/boundary/CommentData.java -0.527468 0.347568 comment
backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java -0.558567 0.262028 comment
backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java -0.403657 0.498037 comment

Wir erzeugen uns eine interaktive Grafik.
Helferlein: An Unifying Software Integrator


In [19]:
from ausi import pygal
xy = pygal.create_xy_chart(dis_2d_df, "module")
xy.render_in_browser()


file://C:/Users/MARKUS~1/AppData/Local/Temp/tmpc0j_ghpu.html

Beyond Hands-On!

Frage 3: Gibt es eine alternative Modularisierung?

Unsere Heuristik: Wie würde sich das System rein nach seinen **Änderungen** strukturieren?

Wir nutzen hierarchisches Clustering, um anhand der Änderungsmuster alternative Modulestrukturen zu erkennen...


In [20]:
from sklearn.cluster import AgglomerativeClustering

clustering = AgglomerativeClustering()
model = clustering.fit(commit_matrix)
model


Out[20]:
AgglomerativeClustering(affinity='euclidean', compute_full_tree='auto',
            connectivity=None, linkage='ward', memory=None, n_clusters=2,
            pooling_func='deprecated')

...und visualisieren das Ergebnis.


In [21]:
from ausi.scipy import plot_dendrogram
plot_dendrogram(model, labels=commit_matrix.index)


https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/062-exploding-head.svg/600px-062-exploding-head.svg.png

Back to the roots...

...denn es geht meist sehr einfach.

Weitere Beispiele

  • Analyse von Performance-Bottlenecks
  • Identifikation von Code-Smells mit Cypher/jQAssistant/Neo4j
  • Code-Call-Analyse mit Groovy/beakerx/tablesaw

Literatur

  • Adam Tornhill: Software Design X-Ray
  • Wes McKinney: Python For Data Analysis
  • Leek, Jeff: The Elements of Data Analytic Style
  • Tim Menzies, Laurie Williams, Thomas Zimmermann: Perspectives on Data Science for Software Engineering

Zusammenfassung

2. Analysen mit Standard-Werkzeugen einfach möglich
2. Wer mehr will bekommt auch mehr!
3. Es gibt unglaublich viele Quellen für Daten in der Softwareentwicklung

Wichtig: Vom **Problem** über die Daten zur Erkenntnis!!

Vielen Dank! Fragen?

Markus Harrer
innoQ Deutschland GmbH

markus.harrer@innoq.com

@feststelltaste

Demos & "Slides": https://github.com/feststelltaste/software-analytics => /demos/20190228_JUG_Nuremberg