Lección 7 - Programación funcional en Spark

Objetivo

El objetivo de esta lección es presentar una introducción a la programación funcional como una herramienta para el análisis de datos.

Programación Funcional

Es un paradigma de programación donde los programas se ejecutan evaluando expresiones, en contraste con la programación imperativa donde los programas se componen de sentencias que alteran el estado del sistema.

Para ello es necesario que las funciones sean de primera-clase, es decir que sean tratadas como cualquier otro valor, puedan ser pasadas como argumentos a otras funciones y puedan ser regresadas como valores a otras funciones.

Características

  • Funciones de primera clase y de orden superior, son funciones que toman otras funciones como argumentos o las regresan como resultado.
  • Funciones puras, son aquellas que no tienen efectos secundarios, en general eso significa que los valores no se modifican, sino que se crean copias de estos valores, preservando los originales.
  • Recursividad, la recursividad es el mecanísmo principal para iterar en los lenguajes funcionales.

El hecho de las funciones no tengan efectos secundarios es muy importante para el procesamiento de datos, ya que permite paralelizar las operaciones.

A continuación presentamos algunos ejemplos de funciones de orden superior. En python, una función lambda se utiliza para definir un objeto de tipo función. Estas funciones tampoco tienen efectos secundarios.


In [9]:
def operate(f, arg1, arg2):
    print f(arg1, arg2)

add = lambda a, b: a + b
sub = lambda a, b: a - b
mul = lambda a, b: a * b
div = lambda a, b: a / b

operate(add, 1, 2)
operate(sub, 1, 2)
operate(mul, 1, 2)
operate(div, 1, 2)


3
-1
2
0

Utilizando PySpark, podremos ver de mejor manera el potencial de los principios de programación funcional.


In [1]:
from pyspark import SparkContext
sc = SparkContext("local[2]", "Functional", pyFiles=[])

Primero definimos el contexto de Spark y creamos una colección paralela. Esto nos permitirá utilizar modismos funcionales desde python.


In [46]:
import math

print 'La función sqrt:', math.sqrt
col = sc.parallelize([1,2,3,4,5])
sqrts = col.map(math.sqrt)
print 'sqrt(x):', sqrts.collect()


La función sqrt: <built-in function sqrt>
sqrt(x): [1.0, 1.4142135623730951, 1.7320508075688772, 2.0, 2.23606797749979]

La función map recibe una función como argumento y aplica la operación definida a cada uno de los elementos de la colección regresando una nueva colección con los valores calculados.

En este ejemplo podemos ver aplicados al menos dos de los principios de programación funcional.

  1. La función math.sqrt se utiliza como un argumento a otra función.
  2. Al aplicar la función map no se está cambiando el estado global del programa, el estado de la variable col no se afecta, sino que regresa una nueva colección con el resultado de aplicar la función math.sqrt

In [47]:
import re

col = sc.parallelize(["hola", "esto", "es", "una", "demostracion"])
lenghts = col.map(len)
count = lenghts.fold(0, add)
print 'len(x):', lenghts.collect()
print 'total:', count

cat = col.fold("", add)
print 'cat:', cat

consonants = col.map(lambda st: re.sub(r"[aeiou]", "", st))
print 'remove [aeiou]:', consonants.collect()


len(x): [4, 4, 2, 3, 12]
total: 25
cat: demostracionunaesestohola
remove [aeiou]: ['hl', 'st', 's', 'n', 'dmstrcn']

En este ejemplo podemos ver como realizar operaciones más elaboradas, por ejemplo la función fold nos permite agregar los resultados utilizando una funcion asociativa y un valor neutral.

$$(0 + (4 + (4 + (2 + (3 + 12))))) = 25$$

Es importante notar el orden en el que se aplica la función fold.

Ahora contemos el número de veces que aparece cada letra en la colección:


In [48]:
words = col.map(list)
print 'split words:', words.collect()

chars = col.flatMap(list)
counts = chars.map(lambda item: (item, 1)).reduceByKey(add)
print 'chars:', chars.collect()
print 'counts:', counts.collect()
print chars.countByKey()


split word: [['h', 'o', 'l', 'a'], ['e', 's', 't', 'o'], ['e', 's'], ['u', 'n', 'a'], ['d', 'e', 'm', 'o', 's', 't', 'r', 'a', 'c', 'i', 'o', 'n']]
chars: ['h', 'o', 'l', 'a', 'e', 's', 't', 'o', 'e', 's', 'u', 'n', 'a', 'd', 'e', 'm', 'o', 's', 't', 'r', 'a', 'c', 'i', 'o', 'n']
counts: [('a', 3), ('c', 1), ('e', 3), ('d', 1), ('i', 1), ('h', 1), ('m', 1), ('l', 1), ('o', 4), ('n', 2), ('s', 3), ('r', 1), ('u', 1), ('t', 2)]
defaultdict(<type 'int'>, {'a': 3, 'c': 1, 'e': 3, 'd': 1, 'i': 1, 'h': 1, 'm': 1, 'l': 1, 'o': 4, 'n': 2, 's': 3, 'r': 1, 'u': 1, 't': 2})

En este ejemplo utilizamos dos funciones nuevas, la función flatMap aplica la función a cada uno de los elementos y después des-empaca las iterables que resultan. reduceByKey agrega los resultados utilizando una función asociativa sobre la llave.


In [3]:
ds1 = sc.parallelize([("mexico", 2), ("australia", 8), ("alemania", 9),
                      ("canada", 1), ("francia", 8)])
ds2 = sc.parallelize([("mexico", "green"), 
                      ("australia", "blue"), 
                      ("alemania", "yellow"),
                      ("canada", "red"),
                     ("canada", "blue"), ("francia", "white")])

dataset = ds1.join(ds2)
print dataset.collect()


[('canada', (1, 'red')), ('canada', (1, 'blue')), ('alemania', (9, 'yellow')), ('francia', (8, 'white')), ('australia', (8, 'blue')), ('mexico', (2, 'green'))]

La función join nos permite unir los elementos de un dataset a partir de una llave.

Ejemplo

A continuación hacemos un análisis sobre las características de una muestra de los usuarios de Twitter


In [6]:
users = sc.textFile('data/users.tsv')
count = users.count()
print 'usuarios:', count


usuarios: 19999

In [11]:
data = users.map(lambda st: st.split('\t')).cache()
print data.take(5)


[[u'12', u'jack', u'Jack Dorsey', u'2536533', u'1071', u'14394', u'en', u'-28800', u'Pacific Time (US & Canada)', u'1', u'0', u'Twitter, Square', u'California'], [u'13', u'biz', u'Biz Stone', u'2093347', u'567', u'5209', u'en', u'-25200', u'Pacific Time (US & Canada)', u'1', u'0', u'Co-founder of Twitter, Inc. + Co-founder & CEO of Jelly', u'San Francisco, CA'], [u'15', u'crystal', u'crystal', u'40085', u'243', u'13962', u'en', u'-28800', u'Tijuana', u'0', u'0', u"don't hate, appreciate", u'San Francisco'], [u'16', u'jeremy', u'Jeremy', u'12046', u'604', u'5390', u'en', u'-28800', u'Pacific Time (US & Canada)', u'0', u'0', u"Lacking pretense isn't always helpful in life. Since 2005.", u'Berkeley CA'], [u'17', u'tonystubblebine', u'Tony Stubblebine', u'16082', u'427', u'7472', u'en', u'-28800', u'Pacific Time (US & Canada)', u'0', u'0', u'Startup nerd. Human potential nerd. CEO and co-founder of @liftapp. Bi-coastal. Often joking.', u'SF/NYC']]

In [10]:
total_followers = data.map(lambda item: int(item[3])).fold(0, add)
total_friends = data.map(lambda item: int(item[4])).fold(0, add)
total_status = data.map(lambda item: int(item[5])).fold(0, add)
print 'average followers: %.2f' % (total_followers / float(count))
print 'average friends: %.2f' % (total_friends / float(count))
print 'average status: %.2f' % (total_status / float(count))


average followers: 4619.24
average friends: 401.43
average status: 4493.79

In [28]:
screen_name_lenght = data.map(lambda item: len(item[1])).fold(0, add)
print 'average screen name: %.2f' % (screen_name_lenght / float(count))


average screen name: 13.64

In [17]:
early_users = data.filter(lambda item: int(item[0]) < 1000)
early_name_lenght = early_users.map(lambda item: len(item[1])).fold(0, add)
print 'average screen name: %.2f' % (early_name_lenght / float(early_users.count()))


average screen name: 6.49

In [20]:
locations = data.map(lambda item: item[8]).map(lambda item: (item, 1)).reduceByKey(add)
top_locations = locations.map(lambda item: (item[1], item[0])).sortByKey(ascending=False)
print top_locations.take(5)


[(9969, u'0'), (2283, u'Eastern Time (US & Canada)'), (1945, u'Pacific Time (US & Canada)'), (1305, u'London'), (965, u'Central Time (US & Canada)')]

In [18]:
early_locations = early_users.map(lambda item: item[12]).map(lambda item: (item, 1)).reduceByKey(add)
top_early_locations = early_locations.map(lambda item: (item[1], item[0])).sortByKey(ascending=False)
print top_early_locations.take(5)


[(18, u'San Francisco'), (17, u''), (16, u'San Francisco, CA'), (6, u'Brooklyn, NY'), (5, u'NYC')]

Finalmente como un ejercicio de clase vamos a obtener cuales son los elementos comunes en las descripciones de los usuarios.


In [37]:
descriptions = data.map(lambda item: item[11])
tokens = descriptions.map(lambda desc: desc.lower())\
    .flatMap(lambda desc: desc.split()) 
token_count = tokens.map(lambda token: (token, 1))\
    .reduceByKey(add)
freq = token_count.map(lambda _: _[1])
total_tokens = freq.fold(0, add)

print total_tokens


121402

In [38]:
%pylab inline


Populating the interactive namespace from numpy and matplotlib
WARNING: pylab import has clobbered these variables: ['add']
`%pylab --no-import-all` prevents importing * from pylab and numpy

In [47]:
fr = sorted(freq.collect(),reverse=True)
plt = plot(fr)
yscale('log')
xscale('log')


Podemos observar que la distribución del lenguaje en las descripciones sigue una ley de Zipf, es decir hay un gran número de palabras que se utilizan muy poco, al mismo tiempo que hay un número pequeño de palabras que se utilizan de manera muy frecuente.


In [ ]: