[Este post también aparece en el blog de github de Dustin].
He estado queriendo describir algo de mi trabajo con el uso de R para ayudarme a entender los datos que estoy recogiendo en Couchbase Server† porque me parece bastante interesante, útil y fácil. Sin embargo, me ha resultado difícil encontrar un buen punto de partida porque no sé quién sería el público. Es decir, encontrar las premisas adecuadas para ponerme en marcha ha sido bastante difícil.
La semana pasada, sin embargo, hablé con un tipo realmente increíble de una empresa de medios de comunicación que tenía una pregunta concreta: "¿Cómo pueden mis analistas informar sobre todos los datos maravillosos que estoy almacenando en Couchbase?" Indagué más. ¿Quiénes son estos analistas? ¿Qué herramientas utilizan?
† las vistas incrementales Map Reduce son idénticas a las vistas Apache CouchDB, por lo que todo funcionará también con CouchDB
Mi público
Resulta que los analistas se acercan bastante a lo que yo imaginaba. Suelen utilizar una especie de herramientas de almacenamiento de datos de Oracle que hacen todo tipo de magia y luego se derrumban si te sales de los límites con los que se sienten cómodos. Parecía algo que podía ignorar. Pero entonces dijo algo que me dio un punto de apoyo bastante sólido. Aunque no son programadores, utilizan R como parte de su análisis de datos.
Debido a que esta pregunta fue hecha por un usuario de Couchbase que quería saber cómo sacar sus datos, voy a asumir que cualquiera que lea esto conoce R un poco mejor que Couchbase.
Acerca de Views
Hay muchas cosas que puedes leer si quieres entender el concepto de vista de sofá. El capítulo sobre la vista del Manual del servidor Couchbase cubre el concepto bastante bien. Si quieres saber todo lo que se puede saber, a continuación, cavar a través de eso, pero para la mayoría de mis usos, lo que realmente se reduce a tres cosas:
- Extrae la información útil.
- Ordenarlo, poner cosas parecidas juntas.
- Haz una agregación básica.
Así es como yo tomo un montón de datos y los convierto en información útil la mayoría de las veces. Espero que los ejemplos que siguen te ayuden a hacer lo mismo.
Los datos
Lo más difícil de cualquier tutorial de grokking de datos es que nunca trata de tus datos. Esto lo hace menos interesante para el lector y, a menudo, más difícil de aplicar a tus propios problemas.
Lamentablemente, los datos más interesantes que extraigo regularmente para elaborar informes son algo sensibles, por lo que no puedo compartir los que más he aprovechado, pero espero que esto te ayude a llegar a algo interesante.
Los datos con los que he decidido trabajar son el conjunto de incidentes notificados por la policía de San Francisco (SFPD) del Página web de SF Data sitio web. Es prácticamente todo lo que la SFPD ha informado desde 2003.
Estos documentos son bastante regulares y planos. Sus datos pueden ser más complicados, pero las técnicas son las mismas. Empecemos por ver un documento de ejemplo del conjunto de datos del SFPD:
"categoría": "Prostitución",
"incident_id": 90096348,
"distrito": "Tenderloin",
"timestamp": “2009-01-27T04:03:00”,
"lon": -122.416261836834,
"lat": 37.7853750846376,
"localización": "Calle Ofarrell / Calle Hyde",
"hora": "04:03",
"date": "2009-01-27",
"Resolución": "Arresto, Citado",
"día": "martes",
"desc": "Solicita Visitar Casa De Prostitución"
}
Creo que puedo entender qué son todas estas cosas, así que manos a la obra.
Introducción de datos en R
Hay algunos paquetes que usaré aquí, así que asegurémonos de tenerlos en tu R antes de irnos:
install.packages(c('rjson', 'ggplot2', 'reshape'),
dependencias=TRUE)
require(rjson)
require(remodelar)
require(RColorBrewer)
require(ggplot2)
Como a R le gustan los datos "cuadrados", tiendo a que la salida de mis vistas sea muy regular, lo que también significa que puedo tener funciones muy simples para tomar una vista y extraerla. Para este propósito, tengo alguna configuración común básica en mis scripts de R que se parece a esto:
# Puntero a tu base de vistas de couchbase. Aquí es donde se encuentra su
# datos propios
urlBase
# Esta es tu petición GET básica -> JSON analizado.
getData fromJSON(file=paste(urlBase, subpath, sep="))$rows
}
# Y esto lo aplana en un marco de datos, opcionalmente nombrando el
Columnas #.
getFlatData b if (!is.null(n)) {
names(b) }
b
}
# Además, voy a trabajar con días de la semana, así que necesito estos:
dow "jueves", "viernes", "sábado")
shortdow
Recuento global de denuncias
Como con la mayoría de los conjuntos de datos, en realidad no sé ni por dónde empezar, así que primero veamos qué tipos de delitos tenemos. Me interesan los recuentos totales y los recuentos por día de la semana. Lo bueno es que con las vistas de sofá, puedo construir una sola vista que me diga cualquiera de las dos cosas. Veamos la fuente de la vista:
emit([doc.category, doc.day], null);
}
Parece muy sencillo, pero combinado con el Cuentareductor incorporado, esto puede hacer un montón de cosas aseadas al agrupar. Con nivel_grupo=1obtenemos el recuento de delitos por categoría. Vamos a trazar eso y ver lo que es popular. Suponiendo que guardamos eso en un documento de diseño llamado categoríascon el nombre de la vista bydayesto es lo que le dices a R:
# Obtener un marco de datos que contenga las categorías y sus respectivos recuentos
cat c('cat', 'count'))
# Las columnas vuelven como cadenas y requiere correcciones para que sea útil
cat$cat cat$count # Además, me di cuenta de que ordenarlo por recuento lo hacía más fácil de entender.
cat$cat
# Ahora trazalo.
ggplot(cat, aes(cat, count, alpha=count)) +
geom_bar(fill='#333399′, stat='identity') +
scale_alpha(to=c(0.4, 0.9), legend=FALSE) +
scale_y_continuous(formatter="comma") +
labs(x=", y=") +
opts(title='Total de denuncias') +
coord_flip() +
tema_bw()
Entonces R te dará esto:

Por día de la semana
Esto me pareció algo interesante, así que quise saber cuál era la distribución por días de la semana. Puedo utilizar la misma vista anterior con nivel_grupo=2Pero como las tasas son tremendamente diferentes, he hecho que R calcule la varianza relativa a través del marco de datos para cada categoría por día de la semana y luego lo he representado gráficamente. Aquí está el código R:
# Coge los mismos datos, pero separados por día de la semana.
cat_byday c('cat', 'day', 'count'))
# Estoy haciendo un arreglo similar al anterior, pero con otro pedido y
# un par de vistas del día de la semana (mucho para jugar)
cat_byday$cat cat_byday$cat cat_byday$count cat_byday$cat_by_count cat_byday$day cat_byday$shortdow
# Calcule el porcentaje de cada categoría según su día de la semana
a1 a2 suma)
total_por_día cat_por_día$perc
# Veamos qué aspecto tiene esto
ggplot(cat_byday, aes(shortdow, cat, size=perc, alpha=perc)) +
geom_point(color=’#333399′) +
scale_alpha(legend=FALSE) +
scale_size(legend=FALSE) +
scale_y_discrete(limits=rev(levels(cat_byday$cat))) +
labs(x=", y=") +
tema_bw()
Eso parece un montón de configuración, pero era sobre todo sólo la configuración de tipo y esas cosas. Vamos a reutilizar algunos a continuación. En este punto, tenemos algo que ver, sin embargo:

Subgrupo específico
Hay muchos datos y todos son relativos, lo que dificulta la comparación. Quería analizar realmente un par de áreas y averiguar qué tipo de correlaciones existían. Como ya tenía los datos cargados, pensé en tomar un subconjunto de lo que ya se había solicitado y graficarlo por facetas.
# Elija algunas categorías de interés
interesante "Prostitución",
'Borrachera',
Conducta desordenada")
# Extrae sólo este subconjunto y refactoriza las categorías
sexo_y_drogas sexo_y_drogas$cat
ggplot(sex_and_drugs, aes(shortdow, count)) +
facet_wrap(~cat, scales='free_y') +
geom_bar(fill='#333399′, stat='identity') +
scale_y_continuous(formatter="comma") +
labs(x=", y=") +
opts(title='Seleccionar informes de delitos por día') +
tema_bw()
Y eso debería mostrarme muchos más detalles sobre estas categorías individuales.

Personalmente, me pareció bastante interesante la falta de correlación entre los incidentes relacionados con el alcohol y los demás. El alcohol parece ser la antidroga. Quizá a las prostitutas no les gustan los borrachos. Quién sabe...
A lo largo del tiempo
En este punto, me he dado cuenta de que los datos se remontan a 2003 y ni siquiera me he planteado si las cosas están mejorando o empeorando. Realmente no he explorado esto mucho, pero quería tener una idea rápida de si las cosas están mejorando o empeorando. Aquí hay una vista que nos dirá las tasas de incidentes por año y categoría:
var ymd = doc.date.split('-');
emit([parseInt(ymd[0], 10), doc.category], null);
}
Como en todos estos ejemplos, lo combino con la función Cuentareducir incorporado. Vamos a trazar las tasas anuales con la siguiente R:
byyear c('year', 'count'))
byyear$year
# Aquí no hay suficiente 2012, así que ignorémoslo para este gráfico.
ggplot(byyear[byyear$year < 2012,], aes(year, count)) +
stat_smooth(fill=’#333399′) +
labs(y=", x=") +
scale_y_continuous(formatter=comma) +
opts(title="Total de informes de incidentes por año") +
tema_bw()
¿Qué nos dice esto?

Parece que las cosas mejoran (o que la policía se vuelve más perezosa). Podría indagar un poco más para averiguar si es cierto para todas las categorías, pero no me interesa tanto, así que veamos otra cosa.
Delincuencia por zonas
Me interesaba saber si determinados delitos eran más populares en unas zonas que en otras. Estoy utilizando el distritopara esto (en lugar de las coordenadas incorporadas) y pensé que podría ser un buen caso de uso para un mapa de calor.
Me he dado cuenta de que algunos informes no tienen un distrito asociado. Decidí ignorarlos para este informe, pero puede ver fácilmente cómo sustituirlo por un valor personalizado si desea tenerlo en cuenta específicamente. Comencemos con el siguiente código de vista:
if (doc.district != null) {
emit([doc.categoría, doc.distrito], null);
}
}
Por supuesto, utilizaremos el Cuentaincorporado de nuevo. Una cosa que debo señalar sobre esto es que mientras yo originalmente trazado todos datos, más tarde decidí que no me interesaba ninguna zona que tuviera menos de 1.000 delitos denunciados. Como se trata de la salida del filtro, necesitaba aplicarlo en R, ya que no tenemos forma de solicitarlo desde una vista (puesto que las vistas están materializadas y la función map no incluía un filtro antes de aplicar la reducción). Lo ideal sería que pudiéramos incluir esto en la solicitud de vista real, pero mientras tanto, podemos extraerlo fácilmente en post:
by_region c('cat', 'region', 'count'))
by_region$count by_region$region
# Ignora todo lo que no tenga al menos 1.000 incidentes
pop_regions 1000,]
pop_regions$cat # Y que los crímenes más calientes flotan a la cima
pop_regions$cat
ggplot(pop_regions, aes(x=region, y=cat, fill=count, alpha=count)) +
geom_tile() +
scale_fill_continuous('Incidentes',
formateador=function(x)
sprintf("%dk", x / 1000)) +
scale_alpha_continuous(legend=FALSE, to=c(0.7, 1.0)) +
labs(x=", y=") +
theme_bw() +
opts(title='Tipos de delitos por distrito',
axis.text.x=texto_temático(angle=-90),
legend.position='derecha')
Esto nos da el siguiente mapa de calor:

Las zonas en blanco no han tenido 1.000 incidentes del tipo de delito especificado en la zona indicada desde 2003. Las zonas de color azul más claro han tenido algunos incidentes. Las de color rojo vivo son las que más. Parece que quiero evitar el distrito sur.
¿Cuántos rechaza el fiscal?
Como ejemplo de la extracción de un agregado del lado del servidor sobre parte de los datos, me pareció especialmente interesante el tipo de resolución "El fiscal del distrito se niega a procesar", por lo que quise saber con qué frecuencia ocurre. De nuevo, empezamos con una vista simple:
emit([doc.resolución, doc.categoría], null);
}
Luego hacemos nuestro Cuentacosa. Sin embargo, la diferencia aquí es que cuando hago la petición, quiero usar el botón clave_inicioy fin_claveparámetros para encontrar sólo las cosas que se resolvieron de esta manera. Resulta que sé que la lista de resoluciones va desde "El fiscal del distrito se niega a procesar" hasta "Autorización excepcional", así que puedo buscar sólo cosas que empiecen por "Di" y terminen con cosas que empiecen por "Dj". Estos también son arrays que estoy emitiendo, por lo que realmente se basa en el primer elemento del array. El código R entonces se ve así:
by_resolution '?group_level=2&start_key=["Di]',
'&end_key=["Dj"]', sep=""),
c('resolución', 'cat', 'recuento'))
by_resolution$count by_resolution$cat by_resolution$cat
ggplot(by_resolution, aes(cat, count, alpha=count)) +
scale_alpha(to=c(0.4, 0.9), legend=FALSE) +
coord_flip() +
geom_bar(fill='#333399′, stat='identity') +
labs(x=", y=") +
opts(title='Delitos que el fiscal se negó a enjuiciar') +
tema_bw()
R nos da entonces lo siguiente:

Tenga en cuenta que se trata de absoluto números. No llame a SF y quejarse porque no se preocupan por asalto como se preocupan por el vandalismo. Simplemente hay más de esos casos. Dejaré como ejercicio al lector evaluar los tipos de resolución por categoría y decidir qué pensar de ellos.
En conclusión
Obviamente podría seguir con esto durante días, pero sólo quería ayudar a la gente a entender mi proceso. En la mayoría de los lugares en los que utilizo esto, los patrones son similares. Los conjuntos de datos pueden crecer mucho, pero las agregaciones siguen siendo pequeñas. El procesamiento incremental de las vistas significa que mis respuestas ordenadas y agregadas siguen llegando rápidamente y que el procesamiento sigue siendo barato.
Parece bastante interesante, pero sería mucho más fácil de leer si se limpiaran los fragmentos de código.
https://dustin.github.com/2012/…
Gracias por el informe. Se comió un poco en el transporte. Curiosamente, en la pantalla de edición se veía bien el código, pero al publicarlo se comía todo.
No soy un programador, pero debo decir que tienes algunos informes agradables allí :)
Buen artículo. Parece que los datos no coinciden entre tu gráfico de puntos y el de subconjuntos. Por ejemplo, en el gráfico de puntos, las detenciones por prostitución son más frecuentes los domingos, martes y viernes, pero en el gráfico de subconjuntos, esas detenciones son más frecuentes los martes, miércoles y jueves.
¿Funciona de forma transparente para todas las funciones y paquetes de R? ¿Produciría un out-of-memory si tratamos con demasiados datos? Por ejemplo, imagina que quiero realizar una regresión por mínimos cuadrados o una prueba ANOVA de 50 GB de datos.