2014-08-22

Importar datos en R: tablas HTML gráficos ggplot2

Title En la entrada anterior creamos un diagrama de dispersión y un histograma usando el paquete estándar graphics. Vamos a crear unos gráficos similares usando ggplot2, no incluidos en la entrada anterior para no hacerla innecesariamente larga.

Ejecutamos el código de los apartados importación y manipulación de la entrada anterior y comenzamos a usar ggplot2.

ggplot2

install.packages("ggplot2") # Instalamos ggplot2
library(ggplot2) # Cargamos el paquete
intervalos <- seq(1920, 2020, 10) # Intervalos
# Diagrama de dispersión
ggplot(peliculas, aes(x = year, y = rating))+
       geom_point(color = "red", size = 3)+
       scale_x_continuous( breaks = seq(0, 3000, 10))+
       theme_classic()
# Histograma personalizado 
ggplot(peliculas, aes(x = year,y = ..count..))+
     geom_histogram(binwidth = 10, colour = "white", fill = "steelblue")+
     ylab(NULL) + ylim(c(0, 60))+
     scale_x_continuous(breaks = intervalos)+
     stat_bin(binwidth = 10, aes(label=..count..), vjust=-0.5, geom = "text")
# Histograma con gradiente de color por nº de películas
ggplot(peliculas, aes(x = year))+
  geom_histogram(binwidth = 10, colour = "white", aes(fill = ..count..))+
  ylab(NULL) + ylim(c(0, 60))+
  scale_x_continuous(breaks = intervalos)

Entradas relacionadas:

2014-08-20

Importar datos en R: tablas HTML

Title Con R podemos leer tablas HTML dentro de un documento HTML. Como ejemplo vamos a la leer la siguiente tabla con las 250 películas más votadas en la imdb.

Importación

Para importar tablas HTML con R necesitamos instalar el paquete XML pensado para leer y crear documentos XML y HTML.

Usamos la función readHTMLTable cuyos argumentos fundamentales son:

url
which: el número de tabla del documento HTML
stringsAsFactors = FALSE para evitar que convierta los caracteres en factores.

library(XML)
url <- "http://www.imdb.com/chart/top" 
peliculas <- readHTMLTable(url, which = 1, stringsAsFactors = FALSE)
Una vez creado el data frame películas, podemos usar las siguientes funciones para inspeccionarlo, editarlo y ver sus propiedades.

head(peliculas) # 6 primeras filas
tail(peliculas) # 6 últimas filas
edit(peliculas) # Abrir en editor de texto
fix(peliculas) # Editar el objeto
dim(peliculas) # Nº filas y columnas
names(peliculas) # Nombre de las columnas
str(peliculas) # Propiedades 

Manipulación

En la imagen anterior vemos que tenemos un problema con los datos importados. La primera y la última columna están en blanco. La segunda contiene la posición (rank), el título (title) y el año (year). La valoración (rating) es un vector de caracteres en lugar de numérico. Varias columnas incluyen \n.

El primer problema que vamos a atacar es dividir la segunda columna en tres: rank, title y year. Para ello empleamos la función colsplit del paquete reshape2.

library(reshape2) # Para usar la función colsplit
peliculas  <-  transform(peliculas, peliculas = colsplit(peliculas[[2]], pattern = "\n", names = c('rank', 'title', 'year')))
Eliminamos las columnas sobrantes y redundantes y les asignamos nuevos nombres.

peliculas[, c(1,2,4,5,6)] <- list(NULL)
colnames(peliculas) <- c("rating", "title", "year")

Aún así seguimos teniendo algunos problemas por resolver. El rating es una vector de caracteres no un vector numérico. En title hay espacios delante y detrás de los títulos. En year los años están entre paréntesis, con espacios delante y detrás, y es un vector de caracteres. Además, las columnas están desordenadas.

peliculas$title <- gsub("^ +|+ $", "", peliculas$title) 
peliculas$year <- gsub("[[:punct:]]|[[:space:]]", "", peliculas$year) 
peliculas$rating <- as.numeric(peliculas$rating)
peliculas$year <- as.integer(peliculas$year)
peliculas <- peliculas[c(2,3,1)]  # Ordena columnas

Gráficos

Representamos los datos para poder ver en que años se concentran las películas mejor valoradas. Empleamos el paquete estándar graphics cargado por defecto. En otra entrada creamos los gráficos con ggplot2.

# Gráficos muy sencillos
plot(x = peliculas$year, y = peliculas$rating) # Gráfico de dispersión
hist(peliculas$year) # Histograma
Con el siguiente diagrama de dispersión se aprecia la concentración de películas en las últimas décadas.

Con el histograma contamos el número de películas dentro de cada década.

Refinamos y personalizamos el histograma para conocer exactamente el número de películas por década.

# Gráfico personalizado
intervalos <- seq(1920, 2020, 10)
hist(peliculas$year,
     breaks = intervalos,
     right = FALSE,
     col = "steelblue",
     border = "white",
     labels = TRUE,
     main = "nº de películas",
     xaxt = "n",         
     yaxt = "n",
     xlab = "Año",        
     ylab = NULL,
     ylim = c(0, 60))
axis(side = 1, at = intervalos)
Para ver gráficos similares generados con el paquete ggplot2 visita esta entrada.

Tabla de frecuencias

Si en lugar de explorar visualmente, creamos directamente una tabla de frecuencias.

intervalos <- seq(1920, 2020, 10)
frecuencias <- cut(peliculas$year,
                   breaks = intervalos,   
                   dig.lab = 4,   # Nº de dígitos de los intervalos
                   right = FALSE) # Intervalos [a, b). Por defecto: (a, b]
tabla.frec  <- table(frecuencias)   # Crea la tabla de frecuencias
as.data.frame(tabla.frec) 
# Tabla de frecuencias
   frecuencias Freq
1  [1920,1930)    4
2  [1930,1940)    7
3  [1940,1950)   15
4  [1950,1960)   30
5  [1960,1970)   21
6  [1970,1980)   21
7  [1980,1990)   30
8  [1990,2000)   43
9  [2000,2010)   54
10 [2010,2020)   25

Observaciones

Suponiendo un nivel de cantidad y calidad constante, esperaríamos unas 25 películas por década. Sin embargo, las películas más recientes están excesivamente representadas. Aún queda la mitad de la década de 2010-2020 y ya hay 25 películas dentro de las 250 mejores.

Posibles factores:

  • La cantidad de películas producidas y conservadas no es constante. Por ejemplo, de acuerdo a David Bordwell, aproximadamente apenas el 20% del cine mudo ha sobrevivido.
  • La calidad, apreciación, acceso y exposición a las películas no son uniformes en el tiempo.
  • IMDb no está disponible desde 1920 para que los usuarios, tras ver la película estrenada, puedan votarla. Se exige un mínimo de 25.000 votos para que una película entre en la lista.
  • IMDb, empresa subsidiaria de Amazon, se centra en los estrenos, trailers y noticias más recientes, no en la evaluación crítica de las películas de la historia del cine.
  • La edad y el perfil de los usuarios que la IMDb considera para elaborar la lista (regular voters).

Otro factor a considerar es el recency effect. Se trata de un sesgo cognitivo que explica que se pondere más, se de más importancia y se recuerde mejor los elementos más recientes.

Referencias:

2014-08-18

Importar datos en R: ficheros de texto

Title La función read.table es la manera más cómoda de importar datos en formato rectangular (hoja de datos) en R. Con ella R importa un fichero en formato rectangular y crea un data frame. Los principales argumentos son:

file - Nombre del fichero importado. Si no se incluye la ruta absoluta, R interpreta que el fichero se encuentra en el directorio de trabajo. También admite una URL completa.
header - Por defecto FALSE. Si la primera fila contiene los nombres de las variables TRUE.
sep - El delimitador, por defecto es sep = ""
quote - Cualificador de texto
dec - Separador decimal

Las funciones read.csv, read.csv2, read.delim, read.delim2 y read.fwf son versiones de la función read.table idénticas salvo por los argumentos que por defecto incluyen. Sirven como atajos o envoltorios (wrappers) con los argumentos apropiados por defecto.

read.csv: para importar ficheros separados por comas, con el punto como separador decimal.
read.csv2: para importar ficheros separados por punto y coma, con la coma como separador decimal.
read.delim: para importar ficheros delimitados por la tabulación con el punto como separador decimal.
read.delim2: para importar ficheros delimitados por la tabulación con la coma como separador decimal. read.fwf: para importar ficheros delimitados por un ancho fijo entre las columnas.

Ejemplos

ruta <- "http://www.cyclismo.org/tutorial/R/_static/simple.csv"
simple <- read.table(ruta, header= TRUE, sep = ",")
head(simple)
ruta <- "http://www.cyclismo.org/tutorial/R/_static/simple.csv"
simple2 <- read.csv(ruta)
head(simple2)
Con read.csv por defecto header = TRUE, por lo que no es necesario especificarlo como con read.table.

  trial mass velocity
1     A 10.0       12
2     A 11.0       14
3     B  5.0        8
4     B  6.0       10
5     A 10.5       13
6     B  7.0       11
read.table transforma las variables de caracteres en factores. Para evitarlo, introducimos el argumento as.is o stringsAsFactors.

# Comprueba que la variable es un factor
str(simple) # Todas las variables o trial
is.factor(x = simple$trial) 
# Evita que read.csv transforme los vectores de caracteres en factores.
simple2 <- read.csv(ruta, as.is = TRUE) 
simple <- read.csv(ruta, stringsAsFactors = FALSE)
Para seleccionar el fichero mediante un cuadro de diálogo usamos la función file.choose

fichero <- read.table(file.choose(), header= TRUE, sep = ",")
fichero <- read.csv(file.choose())

Tras la importación

A continuación algunas funciones para obtener información sobre los datos importados. Parte de la información es redundante.

 # Ver datos
head(simple) # Muestra las 6 primeras filas 
tail(simple) # Muestra las 6 primeras filas 
View(simple) # Abre visor con los datos importados
 # Atributos y estructura
names(simple) # Devuelve los nombres de las variables (columnas)
attributes(simple) # Obtenemos los atributos del objeto
dim(simple) # Número de filas por columnas (observaciones y variables)
sapply(simple, class) # Class type
sapply(simple, mode) # Mode
str(simple) # Estructura del objeto
summary(simple) # Resumen por variable
# Editar los datos
simple <- edit(simple) # Edita y reasigna
fix(simple) # Edita directamente sobre el objeto

En RStudio

1. Environment > Import Dataset > From Text File o From Web URL

2. Seleccionamos el fichero y se abre este cuadro de diálogo:

3. Modificamos las selecciones si fuera necesario y presionamos Import.

RStudio crea un data frame con el nombre del fichero importado y nos lo muestra con la función View. En RStudio el límite de filas mostradas es 1.000.

Referencias:
?read.table

2014-08-15

Listar campos y elementos calculados de una tabla dinámica

Title Si queremos que Excel cree una nueva hoja con los campos y elementos calculados de una tabla dinámica seguimos los siguientes pasos:

1. Clic en cualquier celda de la tabla dinámica.

2. En la parte superior aparecerá Herramientas de tabla dinámica y dos nuevas pestañas, pinchamos sobre la ficha Opciones.

3. Clic sobre el icono Campos, elementos y conjuntos y sobre Crear lista de fórmulas

Excel inserta una nueva hoja en la que lista los campos y elementos calculados de la tabla seleccionada.

VBA

Usamos el método PivotTable.ListFormulas (Excel)

1. Hacemos clic sobre una celda de la tabla dinámica.

2. Ejecutamos la siguiente macro que creará una hoja.

Sub CrearListaCampos()
 On Error GoTo ErrHandler:
 ActiveCell.PivotTable.ListFormulas
 Exit Sub
ErrHandler:
 MsgBox "Selecciona una celda" _
        & " de una tabla dinámica", vbCritical
End Sub
Excel inserta una nueva hoja en la que lista los campos y elementos calculados de la tabla seleccionada. En caso de no haber seleccionado una celda de una tabla dinámica aparecerá el siguiente mensaje de error:

2014-08-12

Listar todos los nombres de libro de Excel

Title Para listar los nombres definidos de un libro de Excel usamos el siguiente código. Crea una nueva hoja en la que lista todos los nombres del libro y autoajusta el ancho de las dos columnas.

Creando una nueva hoja

Sub ListarNombres1()

    Dim wks As Worksheet
    Set wks = Worksheets.Add
    wks.Range("A1").ListNames
    wks.Columns("A:B").Columns.AutoFit

End Sub
Más simplificadamente aún:

Sub ListarNombres2()

    Sheets.Add
    Range("A1").ListNames
    Columns("A:B").AutoFit

End Sub

Cuadro de diálogo

Otra opción que he escrito, consiste en elegir mediante un cuadro de diálogo el rango donde queremos que liste los nombres.

Sub ListarNombres3()

    Dim myRange As Range
    Set myRange = Application.InputBox(Prompt:="Selecciona celda vacía", _
    Title:="Listado de nombres", Type:=8)
    myRange.ListNames
    
End Sub
El inconveniente de la macro anterior es que al copiar el listado de nombres, si no sabemos el número de nombres del libro, puede sobreescribir celdas. Para evitarlo he creado el siguiente código. Crea una hoja temporal con el listado de nombres y, a partir de la celda seleccionada, calcula si el rango necesario para copiar los nombres está vacío o no. Si no lo está, no copiará el listado de nombres.

Sub ListarNombres4() 

    Dim sInicial As String
    sInicial = ActiveSheet.Name
    Dim sName As String
    Worksheets.Add.Name = "temp"
    Sheets("temp").Range("A1").ListNames
    Dim i As Integer
    i = [COUNTA(temp!A:A)]
    Sheets(sInicial).Select
    
    On Error GoTo Final
    Dim myRange As Range
    Set myRange = Application.InputBox(Prompt:="Elige celda con" & _
        " región vacía alrededor de 2 columnas por" & i & " filas", _
        Title:="Listado de nombres", Type:=8)
            If RangeIsBlank(myRange.Resize(i, 2)) = False Then
              MsgBox "Rango no vacío. No se puede copiar aquí.", vbCritical
              GoTo Final
              Else
            End If
    myRange.ListNames
    myRange.Resize(i, 2).Columns.AutoFit
    
Final:
        Application.DisplayAlerts = False
        Worksheets("temp").Delete
        Application.DisplayAlerts = True

End Sub
Para emplear la macro anterior es preciso incluir en un módulo del libro la siguiente función de Dick Kusleika.

Function RangeIsBlank(rRng As Range) As Boolean
    
    If IsNull(rRng.FormulaArray) Then
        RangeIsBlank = False
    Else
        RangeIsBlank = Len(rRng.FormulaArray) = 0
    End If
    
End Function

2014-08-09

Visualizar la historieta XKCD en R

Title

XKCD es una historieta cómica web creada por Randall Munroe dedicada a temas relacionados con la ciencia. Su lema completo es: a webcomic of romance, sarcasm, math, and language. Su principal aliciente y frecuentemente fuente de frustraciones consiste en descifrar los múltiples juegos de alusiones que encierra. Podemos recurrir a explain xkcd donde seguidores del cómic glosan cada una de las historietas.

Paquete XKXD

El paquete RXKCD en R nos permite ver nuestras viñetas de XKCD favoritas directamente en R y buscar palabras claves incluidas en el título o en las notas de la historieta. Básicamente consta de dos funciones:

1. getXKCD

Recupera y muestra la historieta que le indiquemos o bien aleatoriamente.

Los argumentos por defecto son:

getXKCD(which = "current", display = TRUE, html = FALSE,
    saveImg = FALSE)
which: "current" (la última), "random" (aleatoria) o el número de la historieta
display: TRUE para mostrarla en pantalla, o no FALSE
html: TRUE para abrirla en nuestro navegador, o no FALSE
saveImg: guarda la imagen en el directorio actual

Ejemplos:

# Última Historieta publicada
getXKCD()
# Historieta aleatoria
getXKCD(which = "random")
# Historieta 243 mostrada en el navegador
getXKCD(243, html = TRUE)

2. searchXKCD

Localiza historietas con el texto especificado en los títulos y notas de las historietas.

Ejemplo para localizar la historieta relacionada con la anterior: 243

# Busca la cadena keyboards
searchXKCD("keyboards")
# Muestra dos columnas con el número y título de la historieta
  num                    title
1 237 Keyboards are Disgusting
2 243         Appropriate Term

2014-08-06

Moda de serie alfanumérica y duplicados en un rango

Anteriormente vimos cómo calcular el valor más repetido en una serie de datos en la que no hay ningún valor exactamente igual. Esta vez queremos calcular la moda de una serie de valores de texto.

La función MODA no funciona con texto. Tenemos que recurrir introducir otras funciones para obtener el resultado deseado.

C1 = INDICE(A2:A19;MODA(COINCIDIR(A2:A17;A2:A17;0)))
D1 = CONTAR.SI(A2:A19;C2)

Duplicados en un rango

En D1 tenemos el número de repeticiones del texto encontrado en C1. Para obtener el número máximo de repeticiones o duplicados en un rango (tanto texto como números), usamos la siguiente fórmula matricial (Ctrl+Mayús+Entrar):

{=MAX(CONTAR.SI(A2:A19;A2:A19))}

Referencias

Most Frequently Occurring Word

2014-08-03

Consultas combinadas en SQL y Ms Access (join queries)

Title En esta entrada explicaremos las consultas combinadas (join queries). Se denominan así porque combinan filas de dos o más tablas. También explicaremos cómo realizar estas consultas en Ms Access, que utiliza un dialecto de SQL conocido como Jet SQL que presenta algunas diferencias en la sintaxis y expresiones con la versión estándar de SQL.

Nota importante:

Como un abundante número de entradas en la red, explicamos las consultas combinadas mediante diagramas de Venn (usados en teoría de conjuntos). Estos diagramas sirven solamente como analogía y no representan con exactitud el resultado de las consultas combinadas cuando no hay una relación de uno a uno entre las tablas. Cuando la relación entre las tablas es de uno a varios, o de varios a varios, la representación es inadecuada. En estos casos tenemos un nuevo conjunto de filas que satisface las condiciones de la combinación, que no están en ninguna de las tablas, pero que contiene la combinación de columnas de ambas tablas.

Como ejemplo he usado dos tablas empleadas en la wikipedia para explicar estas consultas: tabla employee (empleado) y la tabla department (departamento).

Todos las columnas: * = employee.LastName, employee.DepartmentID, department.DepartmentName

Cross join

Es un producto cartesiano de las dos tablas. Es decir, todas las combinaciones posibles entre las filas de las dos tablas.

SELECT *
FROM department 
CROSS JOIN employee;
Access no permite el comando CROSS JOIN. Por tanto el código anterior generaría un error. Para evitar el error en Access usamos una cross join implícita:

SELECT *
FROM employee, department;
En general, su uso está fuertemente desaconsejado por su capacidad para generar un número enorme de filas. Mil filas en dos tablas generarían un millón de registros. Sin embargo, controladamente son útiles para obtener todas las combinaciones posibles o para crear bases de datos de prueba rápidamente.

Inner join

Compara cada fila de la tabla A (employee) con cada fila de la tabla B (department) para encontrar todos los pares de filas que satisfacen dichas condiciones especificadas.

ON o WHERE preceden la condiciones especificadas.

El diagrama de Venn no ilustra adecuadamente el resultado de la consulta y nos puede conducir a equívoco. El gráfico indica que nuestro conjunto incluirá la intersección de elementos de ambas tablas. Sin embargo, el resultado de la tabla inferior incluye un nuevo conjunto de filas que no pertenecen ni a la tabla A ni a la B. Este nuevo conjunto incluye las columnas de A y B con aquellas filas de ambas tablas que cumplen la condición especificada, que tengan un DepartmentID idéntico. Para cada empleado con el employee.DepartmentID igual al department.DepartmentID las columnas de ambas tablas. Los departamentos Engineering y Clerical aparecen dos veces pues hay dos empleados en la tabla A (employee) que pertenecen a dichos departamentos.

SELECT *
FROM employee 
INNER JOIN department
ON department.DepartmentID = employee.DepartmentID;
Inner join implícita

SELECT *
FROM employee, department
WHERE employee.DepartmentID = department.DepartmentID;

Left outer join

Devuelve todas las filas de la tabla A (employee) incluso si no encuentra ninguna fila en la tabla B (department) que satisfaga la condiciones especificadas. Dicho de otra manera, incluye los resultados de la inner join más aquellas filas de la tabla A (employee) que no coinciden con las de la tabla B (department).

SELECT *
FROM employee 
LEFT JOIN department 
ON employee.DepartmentID = department.DepartmentID;

Left outer join excluyendo la inner join

SELECT *
FROM employee 
LEFT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE department.DepartmentID Is Null;
Right y left outer joins son funcionalmente equivalentes. Ambas proporcionan la misma funcionalidad, de manera que cualquiera de ellas puedes ser reemplazada con la otra con tal de que se invierta el orden de las tablas.

Consulta anterior planteada como una right join (su inversa)

SELECT *
FROM department 
RIGHT JOIN employee 
ON department.DepartmentID = employee.DepartmentID;

Right outer join

Es la consulta inversa de la left outer join. Tan sólo se invierte el orden de las tablas. Devuelve todas las filas de la tabla B (department) incluso si no encuentra ninguna fila en la tabla A (employee) que satisfaga la condiciones especificadas. Dicho de otra manera, incluye los resultados de la inner join más aquellas filas de la tabla B (department) que no coinciden con las de la tabla A (employee).

El diagrama de Venn no representa adecuadamente el resultado de la consulta y nos puede conducir a equívoco. El gráfico nos dice que nuestro conjunto incluirá solamente las filas de la tabla B. Sin embargo, el resultado de la tabla inferior no incluye solamente las 4 filas de la tabla B (department), sino aquellas filas de ambas tablas que cumplen la condición especificada. Muestra cada departamento de la tabla B tantas veces como empleados en la tabla A pertenezcan al mismo. Los departamentos Engineering y Clerical aparecen dos veces pues hay dos empleados en la tabla A (employee) que pertenecen a dichos departamentos.

SELECT *
FROM employee 
RIGHT JOIN department 
ON employee.DepartmentID = department.DepartmentID;
Consulta anterior planteada como una left join (su inversa)

SELECT *
FROM department 
LEFT JOIN employee 
ON employee.DepartmentID = department.DepartmentID;

Right outer join excluyendo la inner join

SELECT *
FROM employee 
RIGHT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE employee.DepartmentID Is Null;

Full outer join

Devolverá todas filas de ambas tablas A (employee) y B (department), satisfagan o no las condiciones especificadas. Para aquellos registros que cumplan las condiciones especificadas devolverá una sola fila con los valores correspondientes de cada tabla.

SELECT *
FROM employee 
FULL OUTER JOIN department
ON employee.DepartmentID = department.DepartmentID;
Ms Access no permite el uso del comando FULL OUTER JOIN. Para obtener el mismo resultado empleamos UNION:

Full outer join = left outer join + right inner join

SELECT *
FROM employee 
LEFT JOIN department 
ON employee.DepartmentID = department.DepartmentID

UNION 

SELECT *
FROM employee 
RIGHT JOIN department 
ON employee.DepartmentID = department.DepartmentID;
Otra alternativa es:

Full outer join = left outer join excluyendo la inner join + inner join + right inner join excluyendo la inner join

SELECT *
FROM employee 
LEFT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE department.DepartmentID Is Null

UNION 

SELECT *
FROM employee 
INNER JOIN department 
ON department.DepartmentID = employee.DepartmentID

UNION

SELECT *
FROM employee 
RIGHT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE (((employee.DepartmentID) Is Null));
Full outer join excluyendo la inner join

SELECT *
FROM employee 
FULL OUTER JOIN department
ON employee.DepartmentID = department.DepartmentID
WHERE employee.ID IS null
OR department.ID IS null;
Como Ms Access no permite el uso del comando FULL OUTER JOIN:

Full outer join = left outer join excluyendo la inner join + right inner join excluyendo la inner join

SELECT *
FROM employee 
LEFT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE department.DepartmentID Is Null

UNION 

SELECT *
FROM employee 
RIGHT JOIN department 
ON employee.DepartmentID = department.DepartmentID
WHERE employee.DepartmentID Is Null;

Referencias

Nube de datos