<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Einführung
Willkommen bei "Komm' wir spielen NSA!"!
Dieses Notebook ist eine kleine Einführung in die Data Science Welt mit Python, dem Notebook-System Jupyter, dem Datenanalyse-Framework Pandas, der Visualisierungsbibliothek "matplotlib" und der Machine-Learning-Bibliothek "scikit-learn".
Die Aufgabe ist, aus dem Mitschnitt von Telefon-Metadaten herauszufinden, wo die von uns verdächtigte Person namens "John Doe" wohnt. Ganz nebenbei lernen wir die wichtigsten Dinge im Umgang mit Python & Co. .
Das Notebook erklärt hierzu immer die Funktionsweisen und die Aufgaben der notwendigen Analyseschritte.
Importieren von Bibliotheken
Als erstes wollen wir über den in Python üblichen Weg das Modul "Pandas" einbinden.
Generell können sog. Module auf mehrere Arten eingebunden werden
Step1:  Interaktive Hilfe
Ob das Importieren des Moduls auch wirklich funktioniert hat, können wir prüfen, in dem wir mit dem pd-Modul arbeiten. Dazu hängen wir an die pd-Variable den ? Operator an und führen die Zelle aus. Es erscheint die Dokumentation des Moduls im unteren Bereich des Notebooks. Diesen Bereich können wir durchlesen und mit der Taste ESC auch wieder verschwinden lassen.
Step2:  Die weitere Funktionalität der Pandas-Bibliothek können wir erkunden, indem wir die Methoden von Pandas ansehen. Dazu verwenden wir pd. und nutzen die integrierte Autovervollständigung von Jupyter mittels der Tabulatortaste Tab, um zu sehen, welche Methoden uns Pandas bietet. Gehen wir dann mit der Pfeiltaste unten z. B. auf Categorical, drücken Enter und schließend Shift+ Tab, dann erscheint die Signatur des entsprechenden Funktionalität und der Ausschnitt der Hilfedokumentation. Bei zweimaligem Drücken von Shift + Tab erscheint die Hilfe vollständig.
Step3:  Laden von Daten
Als nächstes Laden wir Telefon-Metadaten (sog. CDRs
Step4:  Als nächstes sehen wir uns mit der info()-Methode an, was uns nun Pandas in die cdr-Variable geschrieben hat.
Step5:  DataFrame
Wir sehen, das wir nun einen DataFrame erhalten haben. Einen DataFrame können wir uns ersteinmal wie ein Excel-Arbeitsblatt vorstellen. Er besteht aus Zeilen ("rows") und Spalten ("columns"). Wir können auf Zeilen und Spalten Operationen beliebiger Art ausführen lassen. Vor allem die spaltenweise Verarbeitung ist hier sehr effizient gelöst.
Speziell unser DataFrame enthält nun 53188 Einträge von Telefonaten. Wir können das prüfen, indem wir über die in Python eingebauten len-Methode auf den unseren cdr-DataFrame anwenden.
Step6:  Zudem sehen wir Informationen über die Datentypen in dem Dataframe. Die beiden ersten Spalten In und Out sind vom Typ int64, also Ganzzahlen, welche als 64-Bit gespeichert werden (können also sehr große Zahlen werden). Die vier anderen Spalten sind vom Typ object, was in unserem Fall soviel bedeutet, dass wir hier ersteinmal reine Zeichenketten bzw. Texte vorliegen haben.
Alle Spalten werden als non-null angezeigt. D. h. dass in jeder Reihe für jede Spalte immer ein Eintrag vorhanden ist.
Für einzelne Spalten eines DataFrames (den sog. Series) können ganz schnelle Auswertungen vornehmen. Z. B. können wir einmal nachsehen, wie lange die durchschnittliche Dauer aller Telefonate ist. Vorher müssen wir jedoch die Spalte noch in einen passenden Datentyp umwandeln. Pandas bietet uns hier verschiedene Helfermethoden, um dies zu bewerkstelligen
Step7:  Nun können wir auf der Spalte Duration den Durchschnitt mit mean() berechnen.
Step8:  Wir sehen, dass die durchschnittliche Anrufdauer bei etwa sechs Minuten liegt.
Verschneiden mit anderen Daten
Im nächsten Schritt wollen die wir Namen hinter den Telefonnummern herausfinden. Dazu haben wir eine Excel-Datei, welche die entsprechenden Nummern zu den Namen zuordnet namens phoneowners.xlsx im Ordner data.
Step9:  Die Spalten dieser Datei wollen wir nun an den obigen cdr-DataFrame mit anhängen. Hierzu verwenden wir die Funktion pd.merge, welche es uns erlaubt, beliebige Spalten aus unterschiedlichen DataFrames zusammenzuführen.
python
pd.merge(
    left, 
    right,  
    left_on=None, 
    right_on=None,  
    ...)
Das Ergebnis ist ein neuer DataFrame, den wir in der Variable cdr_names speichern. Wir zeigen hier einmal nur die ersten fünf Einträge der relevanten Spalten in der Ausgabe an, in dem wir einen Sub-DataFrame mit den eckigen, doppelten Klammern erzeugen und darauf direkt die head()-Methode aufrufen.
Step10:  Da wir nun wissen, welche Telefonnummer John Doe gehört, können wir die entsprechenden Datensätze herausfinden und alle anderen Datensätze ignorieren. Dazu selektieren wir nun mit Hilfe der Selektor-Schreibweise die entsprechenden Datensätze.
Der Selektor wird mit einer offenen, eckigen Klammer [ begonnen und endet mit einer geschlossenen, eckigen Klammer ]. Innerhalb der eckigen Klammern wir eine Series mit boolschen Werten erwartet, also eine Spalte, die pro Eintrag entweder wahr (True) oder (False) enthält.
Wir schaffen uns solche eine Spalte, in dem wir jetzt nachsehen, ob ein Eintrag in der Spalte name unseres DataFrames cdr_names den Namen "John Doe" enthält.
Step11:  Die obige Hilfevariable können wir nun in den Selektor schreiben und erhalten damit einen DataFrame, welcher nur die Anrufe von John Doe enthält. Das Ergebnis schreiben wir in die Variable john_doe.
Step12:  Im nächsten Schritt interessiert uns, wo John Doe überall telefoniert. Wir wissen dank der TowerId, an welchem Mobilfunkmasten John Doe eingeloggt war, als das Telefonat stattgefunden hatte. In der Datei data/towers.csb haben wir zudem die Informationen, an welcher geografischen Koordinate sich ein Mobilfunkmast befindet. Diese Angaben sind pro TowerId als Breitengrad und Höhengrad abgelegt. Zuerst laden wir die entsprechende CSV-Datei mittels read_csv(). Wir geben zusätzlich mittels index_col=0 mit an, dass wir die erste Spalte im DataFrame als Index verwenden wollen.
Step13:  Zum Zusammenführen der Daten mit unseren bestehenden CDR-DataFrame können wir diesesmal die join()-Methode verwenden, da wir im towers DataFrame einen Index auf die TowerID-Spalte angelegt haben. Die join()-Methode kann nun auf dieser Basis die Daten zusammenführen. Dazu müssen wir noch angeben, welche Spalte im john_doe-DataFrame die Index-Spalte im anderen DataFrame repräsentiert. Das Ergebnis speichern wir in der Variable john_doe_towers.
Step14:  Nun können wir irgendwie nicht mehr erwarten zu sehen, wo die Telefonate durchgeführt wurden. Dazu plotten wir mit der Visualisierungsbibliothek "matplotlib" die entsprechenden Koordinaten der Mobilfunkmasten.
<ASSISTANT_TASK:>
Python Code:
import pandas as pd
Explanation: Einführung
Willkommen bei "Komm' wir spielen NSA!"!
Dieses Notebook ist eine kleine Einführung in die Data Science Welt mit Python, dem Notebook-System Jupyter, dem Datenanalyse-Framework Pandas, der Visualisierungsbibliothek "matplotlib" und der Machine-Learning-Bibliothek "scikit-learn".
Die Aufgabe ist, aus dem Mitschnitt von Telefon-Metadaten herauszufinden, wo die von uns verdächtigte Person namens "John Doe" wohnt. Ganz nebenbei lernen wir die wichtigsten Dinge im Umgang mit Python & Co. .
Das Notebook erklärt hierzu immer die Funktionsweisen und die Aufgaben der notwendigen Analyseschritte.
Importieren von Bibliotheken
Als erstes wollen wir über den in Python üblichen Weg das Modul "Pandas" einbinden.
Generell können sog. Module auf mehrere Arten eingebunden werden:
Einfaches Einbinden mittels import &lt;modul&gt;, z. B. import pandas
Einbinden mit separatem Namen mittels import &lt;modul&gt; as &lt;name&gt;, z. B. import pandas as pd
Einbinden eines Untermoduls mittels from &lt;hauptmodul&gt; import &lt;modul&gt;, z. B. from matplotlib import pyplot
Wir möchten gerne das Modul "Pandas" als pd einbinden. Die Zelle können wir dann mit dem Tastenkürzel Shift + Enter ausführen.
End of explanation
pd?
Explanation: Interaktive Hilfe
Ob das Importieren des Moduls auch wirklich funktioniert hat, können wir prüfen, in dem wir mit dem pd-Modul arbeiten. Dazu hängen wir an die pd-Variable den ? Operator an und führen die Zelle aus. Es erscheint die Dokumentation des Moduls im unteren Bereich des Notebooks. Diesen Bereich können wir durchlesen und mit der Taste ESC auch wieder verschwinden lassen.
End of explanation
pd.Categorical
Explanation: Die weitere Funktionalität der Pandas-Bibliothek können wir erkunden, indem wir die Methoden von Pandas ansehen. Dazu verwenden wir pd. und nutzen die integrierte Autovervollständigung von Jupyter mittels der Tabulatortaste Tab, um zu sehen, welche Methoden uns Pandas bietet. Gehen wir dann mit der Pfeiltaste unten z. B. auf Categorical, drücken Enter und schließend Shift+ Tab, dann erscheint die Signatur des entsprechenden Funktionalität und der Ausschnitt der Hilfedokumentation. Bei zweimaligem Drücken von Shift + Tab erscheint die Hilfe vollständig.
End of explanation
cdr = pd.read_csv('data/CDR_data.csv')
cdr.head()
Explanation: Laden von Daten
Als nächstes Laden wir Telefon-Metadaten (sog. CDRs: Call Data Records) aus einer Datei mit kommaseparierten Daten (einer "CSV"-Datei: "Comma-separated values"). 
Pandas bietet hierzu eine Methode namens read_csv. Diese nimmt als ersten Parameter einen Pfad entgegen. Diverse andere Argumente können angegeben werden, um spezielle Varianten von CSV-Dateien einlesen zu können. Z. B.
* sep: Trennzeichen für Daten in einer Zeile wie etwa sep=";"
* encoding: andere Zeichenkodierungen wie etwa encoding="latin-1"
* dtype: Zuordnung von Spalten zu speziellen Datentypen wie etwa dtype={ 0:str }
Lesen wir erst einmal die Datei data/CDR_data.csv ohne jegliche Argumente ein. Diese Datei enthält die für 10 Mobilfunknutzer gesammelten Telefon-Metadaten über drei Jahre hinweg. Folgende Informationen sind darin enthalten:
In: Die anrufende Telefonnummer 
Out: Die ausgehende Telefonnummer
Direction: Die Richtung des Telefonats
CallTimestamp: Der Zeitstempel des Anrufs
Duration: Die Dauer des Telefonats
TowerID: Die ID des ersten Mobilfunkmasten, der beim Anruf genutzt wurde
Wir lesen die Daten ein und weisen das Ergebnis mittels = der neuen Variable cdr zu. Mittels der Methode head() können wir uns dann in der Folgezeile die ersten fünf Einträge anzeigen lassen.
End of explanation
cdr.info()
Explanation: Als nächstes sehen wir uns mit der info()-Methode an, was uns nun Pandas in die cdr-Variable geschrieben hat.
End of explanation
len(cdr)
Explanation: DataFrame
Wir sehen, das wir nun einen DataFrame erhalten haben. Einen DataFrame können wir uns ersteinmal wie ein Excel-Arbeitsblatt vorstellen. Er besteht aus Zeilen ("rows") und Spalten ("columns"). Wir können auf Zeilen und Spalten Operationen beliebiger Art ausführen lassen. Vor allem die spaltenweise Verarbeitung ist hier sehr effizient gelöst.
Speziell unser DataFrame enthält nun 53188 Einträge von Telefonaten. Wir können das prüfen, indem wir über die in Python eingebauten len-Methode auf den unseren cdr-DataFrame anwenden.
End of explanation
cdr.CallTimestamp = pd.to_datetime(cdr.CallTimestamp)
cdr.Duration = pd.to_timedelta(cdr.Duration)
cdr.info()
Explanation: Zudem sehen wir Informationen über die Datentypen in dem Dataframe. Die beiden ersten Spalten In und Out sind vom Typ int64, also Ganzzahlen, welche als 64-Bit gespeichert werden (können also sehr große Zahlen werden). Die vier anderen Spalten sind vom Typ object, was in unserem Fall soviel bedeutet, dass wir hier ersteinmal reine Zeichenketten bzw. Texte vorliegen haben.
Alle Spalten werden als non-null angezeigt. D. h. dass in jeder Reihe für jede Spalte immer ein Eintrag vorhanden ist.
Für einzelne Spalten eines DataFrames (den sog. Series) können ganz schnelle Auswertungen vornehmen. Z. B. können wir einmal nachsehen, wie lange die durchschnittliche Dauer aller Telefonate ist. Vorher müssen wir jedoch die Spalte noch in einen passenden Datentyp umwandeln. Pandas bietet uns hier verschiedene Helfermethoden, um dies zu bewerkstelligen:
to_numeric wandelt Daten in Zahlen um
to_datetime wandelt Daten in Datumsangaben um
to_timedelta wandelt Daten in Zeitangaben um
Hierzu nutzen wir die Spalte Duration und wenden darauf die Methode mean() an. Den Zugriff auf die Spalte können wir mittels zwei unterschiedlicher Schreibweisen vornehmen:
* Map-Schreibweise: Hier verwenden wir eckige Klammern zum Zugriff auf eine Spalte: cdr['Duration']. Ein wesentlicher Vorteil dieser Schreibweise ist, dass hier jeder Spaltenname (z. B. mit Leerzeichen oder Zahlen) angesprochen werden kann
* Attribut-Schreibweise: Hier können wir Spalte direkt auf dem DataFrame aufrufen: cdr.Duration. Ein Vorteil dieser Schreibweise ist, dass hier die Autovervollständigung funktioniert. Ich bevorzuge diese Schreibweise, soweit das möglich ist.
Wir wandeln die Spalte CallTimestamp mittels pd.to_datetime und die Spalte Duration mittels pd.to_timedelta entsprechend ihrer passenden Datentypen um. Die Ergebnisse speichern wir gleich wieder in die entsprechenden Spalten. Mit info() auf den DataFrame können wir nun kontrollieren, ob die Umwandlung erfolgreich war.
End of explanation
cdr.Duration.mean()
Explanation: Nun können wir auf der Spalte Duration den Durchschnitt mit mean() berechnen.
End of explanation
phone_owners = pd.read_excel("data/phoneowners.xlsx")
phone_owners.head()
Explanation: Wir sehen, dass die durchschnittliche Anrufdauer bei etwa sechs Minuten liegt.
Verschneiden mit anderen Daten
Im nächsten Schritt wollen die wir Namen hinter den Telefonnummern herausfinden. Dazu haben wir eine Excel-Datei, welche die entsprechenden Nummern zu den Namen zuordnet namens phoneowners.xlsx im Ordner data.
End of explanation
cdr_names = pd.merge(cdr, phone_owners, left_on='In', right_on='number')
cdr_names[['In', 'number', 'name']].head()
Explanation: Die Spalten dieser Datei wollen wir nun an den obigen cdr-DataFrame mit anhängen. Hierzu verwenden wir die Funktion pd.merge, welche es uns erlaubt, beliebige Spalten aus unterschiedlichen DataFrames zusammenzuführen.
python
pd.merge(
    left, 
    right,  
    left_on=None, 
    right_on=None,  
    ...)
Das Ergebnis ist ein neuer DataFrame, den wir in der Variable cdr_names speichern. Wir zeigen hier einmal nur die ersten fünf Einträge der relevanten Spalten in der Ausgabe an, in dem wir einen Sub-DataFrame mit den eckigen, doppelten Klammern erzeugen und darauf direkt die head()-Methode aufrufen.
End of explanation
ist_john_doe = cdr_names.name == "John Doe"
ist_john_doe.head()
Explanation: Da wir nun wissen, welche Telefonnummer John Doe gehört, können wir die entsprechenden Datensätze herausfinden und alle anderen Datensätze ignorieren. Dazu selektieren wir nun mit Hilfe der Selektor-Schreibweise die entsprechenden Datensätze.
Der Selektor wird mit einer offenen, eckigen Klammer [ begonnen und endet mit einer geschlossenen, eckigen Klammer ]. Innerhalb der eckigen Klammern wir eine Series mit boolschen Werten erwartet, also eine Spalte, die pro Eintrag entweder wahr (True) oder (False) enthält.
Wir schaffen uns solche eine Spalte, in dem wir jetzt nachsehen, ob ein Eintrag in der Spalte name unseres DataFrames cdr_names den Namen "John Doe" enthält.
End of explanation
john_doe = cdr_names[ist_john_doe]
john_doe.head()
Explanation: Die obige Hilfevariable können wir nun in den Selektor schreiben und erhalten damit einen DataFrame, welcher nur die Anrufe von John Doe enthält. Das Ergebnis schreiben wir in die Variable john_doe.
End of explanation
towers = pd.read_csv("data/towers.csv", index_col=0)
towers.head()
Explanation: Im nächsten Schritt interessiert uns, wo John Doe überall telefoniert. Wir wissen dank der TowerId, an welchem Mobilfunkmasten John Doe eingeloggt war, als das Telefonat stattgefunden hatte. In der Datei data/towers.csb haben wir zudem die Informationen, an welcher geografischen Koordinate sich ein Mobilfunkmast befindet. Diese Angaben sind pro TowerId als Breitengrad und Höhengrad abgelegt. Zuerst laden wir die entsprechende CSV-Datei mittels read_csv(). Wir geben zusätzlich mittels index_col=0 mit an, dass wir die erste Spalte im DataFrame als Index verwenden wollen.
End of explanation
john_doe_towers = john_doe.join(towers, on='TowerID')
john_doe_towers.head()
Explanation: Zum Zusammenführen der Daten mit unseren bestehenden CDR-DataFrame können wir diesesmal die join()-Methode verwenden, da wir im towers DataFrame einen Index auf die TowerID-Spalte angelegt haben. Die join()-Methode kann nun auf dieser Basis die Daten zusammenführen. Dazu müssen wir noch angeben, welche Spalte im john_doe-DataFrame die Index-Spalte im anderen DataFrame repräsentiert. Das Ergebnis speichern wir in der Variable john_doe_towers.
End of explanation
import matplotlib.pyplot as plt
import matplotlib
# sagt Jupyter, dass die Plotausgabe direkt im Notebook passieren soll
%matplotlib inline 
# macht die Plots schöner
matplotlib.style.use('ggplot')
ax = john_doe_towers.plot.scatter(
    x='TowerLon',
    y='TowerLat',
    alpha=0.1,
    title='Anruforte',
    s = 100)
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters = 2)
data = pd.concat([john_doe_towers.TowerLon, john_doe_towers.TowerLat], axis = 1)
labels = kmeans.fit_predict(data)
centroids = kmeans.cluster_centers_
ax.scatter(x = centroids[:, 0], y = centroids[:, 1], c = 'r', marker = 'x', s = 100)
ax.figure
#showandtell()  # TODO: Comment this line out when you're ready to proceed
centroids
joined['CallTimestamp'] = pd.to_datetime(joined['CallTimestamp'])
weekdays = joined['CallTimestamp'].dt.dayofweek.isin(Workweek) & joined['CallTimestamp'].dt.hour.isin(range(8,18))
dfweekdays = joined[weekdays]
dfweekdays.head()
targetname = 'John Doe'
user1 = joined[joined['Name'] == targetname]
#user1 = user1[weekdays]
user1 = user1.reset_index(drop=True)
user1.head()
#
# INFO: The locations map above should be too "busy" to really wrap your head around. This is where domain expertise comes into play.
# Your intuition tells you that people are likely to behave differently on weekends:
#
# On Weekends:
#   1. People probably don't go into work
#   2. They probably sleep in late on Saturday
#   3. They probably run a bunch of random errands, since they couldn't during the week
#   4. They should be home, at least during the very late hours, e.g. 1-4 AM
#
# On Weekdays:
#   1. People probably are at work during normal working hours
#   2. They probably are at home in the early morning and during the late night
#   3. They probably spend time commuting between work and home everyday
#
# TODO: Add more filters to the user1 slice you created. Add bitwise logic so that you're only examining records that came in on
# weekends (sat/sun).
#
# .. your code here ..
user1['DOW'] = user1.CallTimestamp.dt.strftime("%a")
user1 = user1[(user1.DOW == 'Sat') | (user1.DOW == 'Sun')]
user1.head()
#
# TODO: Further filter it down for calls that are came in either before 6AM OR after 10pm (22:00:00). You can use < and > to compare
# the string times, just make sure you code them as military time strings, eg: "06:00:00", "22:00:00":
# https://en.wikipedia.org/wiki/24-hour_clock
#
# You might also want to review the Data Manipulation section for this. Once you have your filtered slice, print out its length:
#
# .. your code here ..
user1 = user1[(user1.CallTimestamp < "06:00:00") | (user1.CallTimestamp > "22:00:00")]
user1.head()
#
# INFO: Visualize the dataframe with a scatter plot as a sanity check. Since you're familiar with maps, you know well that your
# X-Coordinate should be Longitude, and your Y coordinate should be the tower Latitude. Check the dataset headers for proper column
# feature names.
# https://en.wikipedia.org/wiki/Geographic_coordinate_system#Geographic_latitude_and_longitude
#
# At this point, you don't yet know exactly where the user is located just based off the cell phone tower position data; but
# considering the below are for Calls that arrived in the twilight hours of weekends, it's likely that wherever they are bunched up
# is probably near the caller's residence:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(user1.TowerLon,user1.TowerLat, c='g', marker='o', alpha=0.2)
ax.set_title('Weekend Calls (<6am or >10p)')
#showandtell()  # TODO: Comment this line out when you're ready to proceed
#
# TODO: Run K-Means with a K=1. There really should only be a single area of concentration. If you notice multiple areas that are
# "hot" (multiple areas the usr spends a lot of time at that are FAR apart from one another), then increase K=2, with the goal being
# that one of the centroids will sweep up the annoying outliers; and the other will zero in on the user's approximate home location.
# Or rather the location of the cell tower closest to their home.....
#
# Be sure to only feed in Lat and Lon coordinates to the KMeans algo, since none of the other data is suitable for your purposes.
# Since both Lat and Lon are (approximately) on the same scale, no feature scaling is required. Print out the centroid locations and
# add them onto your scatter plot. Use a distinguishable marker and color.
#
# Hint: Make sure you graph the CORRECT coordinates. This is part of your domain expertise.
#
# .. your code here ..
#coordinates = "" + centroids[0][1].to_string().split('.')[0] + "°" + centroids[0][1].split('.')[1][0]  + centroids[0][1].split('.')[1][1] + centroids[0][0]
#str.split(' ', 1 )
centroids
difference1 = centroids[0][1] - centroids[1][1]
difference2 = centroids[0][0] - centroids[1][0]
difference1 = 0.5 * difference1
difference2 = 0.5 * difference2
coordinate1 = centroids[0][1] + difference1
coordinate2 = centroids[0][0] + difference2
coordinates = str(coordinate1) + " " + str(coordinate2)
coordinates
#-96°90'92.4672"N  96°56'57.3"W
Explanation: Nun können wir irgendwie nicht mehr erwarten zu sehen, wo die Telefonate durchgeführt wurden. Dazu plotten wir mit der Visualisierungsbibliothek "matplotlib" die entsprechenden Koordinaten der Mobilfunkmasten.
End of explanation
<END_TASK>