-
Notifications
You must be signed in to change notification settings - Fork 1
Home
En esta práctica se elabora un caso práctico orientado a aprender a identificar los datos relevantes para un proyecto analítico y usar las herramientas de integración, limpieza, validación y análisis de las mismas. Para hacer esta práctica tendréis que trabajar en grupos de 2 personas. Tendréis que entregar un solo archivo con el enlace Github (https://github.com) donde se encuentren las soluciones incluyendo los nombres de los componentes del equipo. Podéis utilizar la Wiki de Github para describir vuestro equipo y los diferentes archivos que corresponden a vuestra entrega. Cada miembro del equipo tendrá que contribuir con su usuario Github. Aunque no se trata del mismo enunciado, los siguientes ejemplos de ediciones anteriores os pueden servir como guía:
- Ejemplo: https://github.com/Bengis/nba-gap-cleaning
- Ejemplo complejo (archivo adjunto).
En esta práctica se desarrollan las siguientes competencias del Máster de Data Science:
- Capacidad de analizar un problema en el nivel de abstracción adecuado a cada situación y aplicar las habilidades y conocimientos adquiridos para abordarlo y resolverlo.
- Capacidad para aplicar las técnicas específicas de tratamiento de datos (integración, ransformación, limpieza y validación) para su posterior análisis.
Los objetivos concretos de esta práctica son:
- Aprender a aplicar los conocimientos adquiridos y su capacidad de resolución de problemas en entornos nuevos o poco conocidos dentro de contextos más amplios o multidisciplinares.
- Saber identificar los datos relevantes y los tratamientos necesarios (integración, limpieza y validación) para llevar a cabo un proyecto analítico.
- Aprender a analizar los datos adecuadamente para abordar la información contenida en los datos.
- Identificar la mejor representación de los resultados para aportar conclusiones sobre el problema planteado en el proceso analítico.
- Actuar con los principios éticos y legales relacionados con la manipulación de datos en función del ámbito de aplicación.
- Desarrollar las habilidades de aprendizaje que les permitan continuar estudiando de un modo que tendrá que ser en gran medida autodirigido o autónomo.
- Desarrollar la capacidad de búsqueda, gestión y uso de información y recursos en el ámbito de la ciencia de datos.
En este apartado resummiremos los pasos necesarios para la preparación del dataset final realizado en la PRA 1:
El dataset seleccionado ha sido obtenido desde el siguiente enlace. Este juego de datos contiene los resultados de la Encuesta Pública para Desarrolladores de StackOverflow 2020. Se obtuvo alrededor de 65000 participaciones de programadores y desarrolladores de 180 paises. La encuesta aborda varios ámbitos, tanto a nivel de experiencia, formación académica y skills (habilidades técnicas) en diferentes tecnologías que el encuestado ha ido adquiriendo a lo largo del tiempo.
Esta encuesta anual ha recolectado datos sobre 61 variables que se pasan a detallar a continuación:
- Respondent: número de identificación del encuestado aleatorizado (no en orden de tiempo de respuesta de la encuesta)
- MainBranch: ¿Cuál de las siguientes opciones te describe mejor hoy?
- Hobbyist: ¿Desarrollas como pasatiempo?
- Age: ¿Cuál es su edad (en años)?
- Age1stCode: ¿A qué edad escribiste tu primera línea de código o programa?
- CompFreq: ¿Esa compensación es semanal, mensual o anual?
- CompTotal: ¿Cuál es su compensación total actual (salario, bonificaciones y beneficios, antes de impuestos y deducciones), en “CurrencySymbol”?. Número entero.
- ConvertedComp: Salario anual en USD, utilizando el tipo de cambio del 19 de febrero de 2020, asumiendo 12 meses laborales y 50 semanas laborales".
- Country: País dónde vive.
- CurrencyDesc: ¿Qué moneda utiliza a diario? Descripción.
- CurrencySymbol: ¿Qué moneda usa a diario? Forma abreviada.
- DatabaseDesireNextYear: ¿En qué entornos de base de datos desea trabajar durante el próximo año?
- DatabaseWorkedWith: ¿En qué entornos de base de datos ha realizado un trabajo de desarrollo extenso durante el año pasado?
- DevType: ¿Cuál de los siguientes lo describe?
- EdLevel: ¿Cuál de las siguientes opciones describe mejor el nivel más alto de educación formal que ha completado?
- Employment: ¿cuál de las siguientes opciones describe mejor su situación laboral actual?
- Ethnicity: ¿Cuál de los siguientes grupos étnicos lo describe?
- Gender: ¿Cuál de las siguientes opciones de sexo lo describe?
- JobFactors: Para el caso de decidiendo entre dos ofertas de trabajo con la misma compensación, beneficios y ubicación. ¿Qué factores son los más importantes para usted?
- JobSat: ¿Qué tan satisfecho está con su trabajo actual?
- JobSeek: ¿Cuál de las siguientes opciones describe mejor su estado actual de búsqueda de empleo?
- LanguageDesireNextYear: "¿En qué lenguajes de programación, scripting y marcado desea trabajar durante el próximo año?.
- LanguageWorkedWith: ¿En qué lenguajes de programación, scripting y marcado ha realizado un trabajo de desarrollo extenso durante el año pasado?.
- MiscTechDesireNextYear: ¿En qué otros frameworks, bibliotecas y herramientas desea trabajar durante el próximo año?.
- MiscTechWorkedWith: ¿En qué otros frameworks, bibliotecas y herramientas ha realizado un trabajo de desarrollo extenso durante el año pasado?.
- NEWCollabToolsDesireNextYear: ¿En qué herramientas de colaboración desea trabajar durante el próximo año?
- NEWCollabToolsWorkedWith: ¿En qué herramientas de colaboración ha realizado un trabajo de desarrollo extenso durante el año pasado?
- NEWDevOps: ¿Su empresa tiene una persona dedicada a DevOps?
- NEWDevOpsImpt: ¿Qué importancia tiene la práctica de DevOps para escalar el desarrollo de software?
- NEWEdImpt: ¿Qué importancia tiene una educación formal, como un título universitario en ciencias de la computación, para su carrera?
- NEWJobHunt: En general, ¿Cuáles son las motivaciones que lo impulsan a buscar un nuevo trabajo?.
- NEWJobHuntResearch: Cuando busca trabajo, ¿cómo puede obtener más información sobre una empresa?
- NEWLearn: ¿Con qué frecuencia aprende un nuevo lenguaje o marco?
- NEWOffTopic: ¿Crees que Stack Overflow debería relajar las restricciones sobre lo que se considera “fuera de tema”?
- NEWOnboardGood: ¿Cree que su empresa tiene un buen proceso de incorporación? (Por incorporación, nos referimos al proceso estructurado para que se adapte a su nuevo puesto en una empresa)
- NEWOtherComms: ¿Es miembro de alguna otra comunidad de desarrolladores en línea?
- NEWOvertime: ¿Con qué frecuencia trabaja horas extraordinarias o más allá de las expectativas formales de su trabajo?
- NEWPurchaseResearch: Al comprar una nueva herramienta o software, ¿cómo descubre e investiga las soluciones disponibles?
- NEWPurpleLink: Busca una solución de codificación en línea y el primer enlace de resultado es violeta porque ya lo visitó. ¿Cómo se siente?
- NEWSOSites: ¿Cuál de los siguientes sitios de Stack Overflow ha visitado?
- NEWStuck: ¿Qué hace cuando se queda atascado en un problema?
- OpSys: ¿Cuál es el sistema operativo principal en el que trabaja?
- OrgSize: Aproximadamente, ¿cuántas personas emplea la empresa u organización para la que trabaja actualmente?
- PlatformDesireNextYear: ¿En qué plataformas desea trabajar durante el próximo año?
- PlatformWorkedWith: ¿En qué plataformas ha realizado un trabajo de desarrollo extenso durante el año pasado?
- PurchaseWhat: ¿Qué nivel de influencia tiene usted, personalmente, sobre las compras de nueva tecnología en su organización?
- Sexuality: ¿Cuál de los siguientes lo describe a usted sobre su sexualidad?.
- SOAccount: ¿Tiene una cuenta de Stack Overflow?
- SOComm: ¿Te consideras miembro de la comunidad de Stack Overflow?
- SOPartFreq: ¿Con qué frecuencia diría que participa en preguntas y respuestas en Stack Overflow? Por participar nos referimos a preguntar, responder, votar o comentar preguntas.
- SOVisitFreq: ¿Con qué frecuencia visita Stack Overflow?
- SurveyEase: ¿Qué tan fácil o difícil fue completar esta encuesta?
- SurveyLength: ¿Qué opina de la duración de la encuesta este año?
- Trans: ¿Eres transgénero?
- UndergradMajor: ¿Cuál fue su campo de estudio principal?
- WebframeDesireNextYear: ¿En qué frameworks web desea trabajar durante el próximo año?
- WebframeWorkedWith: ¿En qué frameworks web ha realizado un extenso trabajo de desarrollo durante el año pasado?
- WelcomeChange: En comparación con el año pasado, ¿qué tan bienvenido se siente en Stack Overflow?
- WorkWeekHrs: En promedio, ¿cuántas horas por semana trabaja?
- YearsCode: Incluyendo cualquier educación, ¿cuántos años ha estado programando en total?
- YearsCodePro: NO incluye educación, ¿cuántos años ha programado profesionalmente (como parte de su trabajo)? Las capacidades análiticas del dataset, que se tomaron en cuenta para elegirlo son:
Cuenta con una cantidad suficientes variables, tanto numéricas, categóricas. Las variables categóricas también pueden volverse a convertir a variables numéricas. Esto permitiría aplicar algoritmos supervisados y no supervisados, donde se puede clasificar a los programadores o desarrolladores según la experticia actual.
También permite agregar nuevas variables númericas que representen el número de tecnologías que domina cada encuestado.
Al incluir las tecnologías usadas por desarrolladores en: base de datos, lenguages de programación, frameworks y demás herramientas, permite tener una gran cantidad de preferencias de las que se puede extraer reglas de asociación interesantes sobre las tecnologías más usadas entre los distintos tipos de desarrolladores.
Cuenta con variables que pueden discretizarse y otras donde se puede aplicar tareas de limpieza y preparación previa antes de aplicar los distintos métodos.
Sin embargo, para efectos del análisis, del dataset original, se excluirán las siguientes variables:
- Age
- ConvertedComp
- Country
- DatabaseWorkedWith
- EdLevel
- Employment
- LanguageWorkedWith
- MiscTechWorkedWith
- NEWCollabToolsWorkedWith
- NEWLearn
- NEWOvertime
- OpSys
- PlatformWorkedWith
- SOPartFreq
- UndergradMajor
- WebframeWorkedWith
- WorkWeekHrs
- YearsCodePro
Muchos de estos campos no son relevantes para el alcance de la Práctica #2; otros reflejan deseos de los programadores respecto a tecnologías, para lo cual solo tomaremos los datos que reflejan la experiencia actual del programador.
Vamos a trabajar preliminarmente con 18 variables propias del dataset original, de las cuales 3 son númericas (Age, ConvertedComp y WorkWeekHrs). También tenemos variables no numéricas, las cuales vamos a realizar un análisis más detallado posteriormente, generando variables númericas a partir de ellas, las cuales son:
- DatabaseWorkedWith
- LanguageWorkedWith
- MiscTechWorkedWith
- NEWCollabToolsWorkedWith
- PlatformWorkedWith
- WebframeWorkedWith
Estas nuevas variables numéricas a generarse posteriormente servirán principalmente cuando se defina el número total de tecnologías conocidas y usadas por los encuestados.
Este dataset ayudará a dar respuesta a la siguientes interrogantes:
- ¿Existe una fuerte correlación entre las variables seleccionadas en nuestro dataset?
- ¿Cuáles son las variables que influyen en el salario de los entrevistados de la comunidad Stack Overflow?
# Cargamos el dataset completo
data_devs <- read.csv('../data/survey_results_public.csv', sep=",", encoding = "UTF-8")
# Resumen del dataset original
head(data_devs)
# Creamos un juego de datos resumido
data_so <- data_devs[, c(4, 8, 9, 13, 15, 16, 23, 25, 27, 33, 37, 42, 45, 50, 55, 57, 59, 61)]
head(data_so)
Se ha revisado que existen valores nulos en todas las variables seleccionadas, por lo que se procederá a filtrar los registros que no contengan valores NA en ninguna de las variables.
Además, en este apartado se ha decidido filtrar los valores que no representan una realidad en el estudio de la variable en cuestión. Por ejemplo un valor de 0 en el sueldo anual (variable ConvertedComp) de un desarrollador no representa la realidad. A pesar de ser un valor que puede ser tratado como un valor extremo, para esta ocasión se lo tratará como no válido.
# Eliminar filas con valores nulos
data_wo_na <- data_so[!is.na(data_so$Age), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$ConvertedComp), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$Country), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$DatabaseWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$EdLevel), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$Employment), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$LanguageWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$MiscTechWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$NEWCollabToolsWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$NEWLearn), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$NEWOvertime), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$OpSys), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$PlatformWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$SOPartFreq), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$UndergradMajor), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$WebframeWorkedWith), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$WorkWeekHrs), ]
data_wo_na <- data_wo_na[!is.na(data_wo_na$YearsCodePro), ]
# Campos con valores cero
data_wo_na <- data_wo_na[data_wo_na$ConvertedComp != 0, ]
head(data_wo_na)
Analizamos el tipo de dato de las columnas antes mencionadas
library(dplyr)
#Vemos el tipo de dato de las variables
glimpse(data_wo_na)
# Otra forma de ver el tipo de dato de cada columna
sapply(data_wo_na, class)
Para la variable YearsCodePro observamos que el tipo de datos es “character”, por lo que contiene valores cualitativos: More than 50 years y Less than 1 year, por lo que vamos a transformarlos en valores numéricos o cuantitativos
# Convertimos los valores categoricos en numéricos para la variable YearsCodePro
data_temp <- data_wo_na[data_wo_na$YearsCodePro %in% c("More than 50 years", "Less than 1 year"), ]
dim(data_temp)
Existen 375 valores con valores cualitativos que deben ser transformados a valores cuantitativos
# Convertimos los valores categoricos en numéricos para la variable YearsCodePro
data_wo_na$YearsCodePro[data_wo_na$YearsCodePro=="Less than 1 year"] <- 1
data_wo_na$YearsCodePro[data_wo_na$YearsCodePro=="More than 50 years"] <- 50
# Finalmente convertimos dicha columna en númerica
data_wo_na$YearsCodePro <- as.numeric(data_wo_na$YearsCodePro)
filas_df=dim(data_wo_na)[1]
Ahora vamos a agregar nuevas variables que contabilizan el número de tecnologías o herramientas de: bases de datos, lenguajes de programación, de colaboración, entre otros. Primero para la base de datos, vamos a usar la columna DatabaseWorkedWith. La variable a crearse será db_techs:
data_wo_na$db_techs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$DatabaseWorkedWith[i])) {
data_wo_na$db_techs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$DatabaseWorkedWith[i], ";"), length)
data_wo_na$db_techs[i] <- longitud
}
}
Para agregar una nueva variable que represente el número de lenguajes de programación que usa. Este dato se basa en la experiencia ya adquirida y no en los deseos para usar o aprender el siguiente año. Para esto usaremos la columna LanguageWorkedWith. La variable a crearse será prog_langs:
data_wo_na$prog_langs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$LanguageWorkedWith[i])) {
data_wo_na$prog_langs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$LanguageWorkedWith[i], ";"), length)
data_wo_na$prog_langs[i] <- longitud
}
}
Ahora vamos a agregar una nueva variable para el número de frameworks, librerías y demás herramientras que usa el desarrollador. Este dato se basa en la experiencia ya adquirida y no en los deseos para usar o aprender el siguiente año. Para esto usaremos la columna MiscTechWorkedWith. La variable a crearse será misc_techs:
data_wo_na$misc_techs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$MiscTechWorkedWith[i])) {
data_wo_na$misc_techs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$MiscTechWorkedWith[i], ";"), length)
data_wo_na$misc_techs[i] <- longitud
}
}
Haremos lo mismo para el número de herramientras colaborativas que usa el desarrollador, según el contenido de la columna NEWCollabToolsWorkedWith. Este dato se basa en la experiencia ya adquirida y no en los deseos para usar o aprender el siguiente año. La variable a crearse será collab_techs:
data_wo_na$collab_techs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$NEWCollabToolsWorkedWith[i])) {
data_wo_na$collab_techs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$NEWCollabToolsWorkedWith[i], ";"), length)
data_wo_na$collab_techs[i] <- longitud
}
}
También vamos a agregar una variable para el número de plataformas que usa el desarrollador. Este dato se basa en la experiencia ya adquirida y no en los deseos para usar o aprender el siguiente año. Usaremos el contenido de la columna PlatformWorkedWith. La variable a crearse será plat_techs:
data_wo_na$plat_techs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$PlatformWorkedWith[i])) {
data_wo_na$plat_techs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$PlatformWorkedWith[i], ";"), length)
data_wo_na$plat_techs[i] <- longitud
}
}
Finalmente, agregaremos una variable para el número de frameworks web que usa el desarrollador. Este dato se basa en la experiencia ya adquirida y no en los deseos para usar o aprender el siguiente año. Para esto usaremos la columna WebframeWorkedWith. La variable a crearse será web_techs:
data_wo_na$web_techs <- 0
for(i in 1:filas_df) {
if (is.na(data_wo_na$WebframeWorkedWith[i])) {
data_wo_na$web_techs[i] <- 0
} else {
longitud <- sapply(strsplit(data_wo_na$WebframeWorkedWith[i], ";"), length)
data_wo_na$web_techs[i] <- longitud
}
}
Eliminamos las variables cualitativas previamente tratadas
data_wo_na$DatabaseWorkedWith = NULL
data_wo_na$LanguageWorkedWith = NULL
data_wo_na$MiscTechWorkedWith = NULL
data_wo_na$NEWCollabToolsWorkedWith = NULL
data_wo_na$PlatformWorkedWith = NULL
data_wo_na$WebframeWorkedWith = NULL
La variable EdLevel tiene valores como cadenas de texto muy extensas que dificultan su legibilidad, para lo cual vamos a realizar una reasignación de valores, con nomenclaturas más cortas, para lo cual procederemos de la siguiente manera:
- “I never completed any formal education” -> “NEVER”
- “Primary/elementary school” -> “PRIMARY”
- “Secondary school (e.g. American high school, German Realschule or Gymnasium, etc.)” -> “SECONDARY”
- “Some college/university study without earning a degree” -> “SOME_STUDY_WITHOUT_DEGREE”
- “Associate degree (A.A., A.S., etc.)” -> “ASSOCIATE”
- “Bachelor’s degree (B.A., B.S., B.Eng., etc.)” -> “BACHELOR”
- “Master’s degree (M.A., M.S., M.Eng., MBA, etc.)” -> “MASTER”
- “Professional degree (JD, MD, etc.)” -> “PROFESSIONAL”
- “Other doctoral degree (Ph.D., Ed.D., etc.)” -> “OTHER_PHD”
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="I never completed any formal education"] <- 'NEVER'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Primary/elementary school"] <- 'PRIMARY'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Secondary school (e.g. American high school, German Realschule or Gymnasium, etc.)"] <- 'SECONDARY'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Some college/university study without earning a degree"] <- 'SOME_STUDY_WITHOUT_DEGREE'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Associate degree (A.A., A.S., etc.)"] <- 'ASSOCIATE'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Bachelor’s degree (B.A., B.S., B.Eng., etc.)"] <- 'BACHELOR'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Master’s degree (M.A., M.S., M.Eng., MBA, etc.)"] <- 'MASTER'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Professional degree (JD, MD, etc.)"] <- 'PROFESSIONAL'
# Reasigamos el valor
data_wo_na$EdLevel[data_wo_na$EdLevel=="Other doctoral degree (Ph.D., Ed.D., etc.)"] <- 'OTHER_PHD'
table(as.factor(data_wo_na$EdLevel))
plot(as.factor(data_wo_na$EdLevel))
De la misma manera procedemos con la variable Employment, para lo cual vamos a realizar una reasignación de valores, con nomenclaturas más cortas, para lo cual procederemos de la siguiente manera:
“Employed full-time” -> “FULL_TIME” “Employed part-time” -> “PART_TIME” “Independent contractor, freelancer, or self-employed” -> “FREELANCER” “Not employed, but looking for work” -> “NOT_EMPLOYED_LOOKING_FOR” “Not employed, and not looking for work” -> “NOT_EMPLOYED_NOT_LOOKING_FOR” “Student” -> “STUDENT” “Retired” -> “RETIRED”
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Employed full-time"] <- 'FULL_TIME'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Employed part-time"] <- 'PART_TIME'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Independent contractor, freelancer, or self-employed"] <- 'FREELANCER'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Not employed, but looking for work"] <- 'NOT_EMPLOYED_LOOKING_FOR'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Not employed, and not looking for work"] <- 'NOT_EMPLOYED_NOT_LOOKING_FOR'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Student"] <- 'STUDENT'
# Reasigamos el valor
data_wo_na$Employment[data_wo_na$Employment=="Retired"] <- 'RETIRED'
table(as.factor(data_wo_na$Employment))
plot(as.factor(data_wo_na$Employment))
Para la variable NEWOvertime vamos a realizar una reasignación de valores, con valores más cortos, para lo cual procederemos de la siguiente manera:
“Never” -> “NEVER” “Occasionally: 1-2 days per quarter but less than monthly” -> “OCCASIONALLY” “Often: 1-2 days per week or more” -> “OFTEN” “Rarely: 1-2 days per year or less” -> “RARELY” “Sometimes: 1-2 days per month but less than weekly” -> “SOMETIMES”
# Reasigamos el valor
data_wo_na$NEWOvertime[data_wo_na$NEWOvertime=="Never"] <- 'NEVER'
# Reasigamos el valor
data_wo_na$NEWOvertime[data_wo_na$NEWOvertime=="Occasionally: 1-2 days per quarter but less than monthly"] <- 'OCCASIONALLY'
# Reasigamos el valor
data_wo_na$NEWOvertime[data_wo_na$NEWOvertime=="Often: 1-2 days per week or more"] <- 'OFTEN'
# Reasigamos el valor
data_wo_na$NEWOvertime[data_wo_na$NEWOvertime=="Rarely: 1-2 days per year or less"] <- 'RARELY'
# Reasigamos el valor
data_wo_na$NEWOvertime[data_wo_na$NEWOvertime=="Sometimes: 1-2 days per month but less than weekly"] <- 'SOMETIMES'
table(as.factor(data_wo_na$NEWOvertime))
plot(as.factor(data_wo_na$NEWOvertime))
Para la variable SOPartFreq vamos a realizar una reasignación de valores, con valores más cortos, para lo cual procederemos de la siguiente manera:
“Less than once per month or monthly” -> “LESS_ONCE_MONTH” “A few times per month or weekly” -> “FEW_TIMES_MONTH” “Multiple times per day” -> “MULTIPLE_TIMES_DAY” “I have never participated in Q&A on Stack Overflow” -> “NEVER” “A few times per week” -> “FEW_TIMES_WEEK” “Daily or almost daily” -> “DAILY”
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="Less than once per month or monthly"] <- 'LESS_ONCE_MONTH'
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="A few times per month or weekly"] <- 'FEW_TIMES_MONTH'
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="Multiple times per day"] <- 'MULTIPLE_TIMES_DAY'
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="I have never participated in Q&A on Stack Overflow"] <- 'NEVER'
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="A few times per week"] <- 'FEW_TIMES_WEEK'
# Reasigamos el valor
data_wo_na$SOPartFreq[data_wo_na$SOPartFreq=="Daily or almost daily"] <- 'DAILY'
table(as.factor(data_wo_na$SOPartFreq))
plot(as.factor(data_wo_na$SOPartFreq))
Para la variable UndergradMajor vamos a realizar una reasignación de valores, con valores más cortos, para lo cual procederemos de la siguiente manera:
“Computer science, computer engineering, or software engineering” -> “COMPUTER_SCIENCE” “Web development or web design” -> “WEB_DEVELOPMENT” “Information systems, information technology, or system administration” -> “INFORMATION_SYSTEMS” “Mathematics or statistics” -> “MATHS_STATS” “Another engineering discipline (such as civil, electrical, mechanical, etc.)” -> “ANOTHER_ENGINEERING_DISCIPLINE” “A business discipline (such as accounting, finance, marketing, etc.)” -> “BUSINESS” “A health science (such as nursing, pharmacy, radiology, etc.)” -> “HEALTH” “A humanities discipline (such as literature, history, philosophy, etc.)” -> “HUMANITIES” “A natural science (such as biology, chemistry, physics, etc.)” -> “NATURAL_SCIENCE” “A social science (such as anthropology, psychology, political science, etc.)” -> “SOCIAL_SCIENCE” “Fine arts or performing arts (such as graphic design, music, studio art, etc.)” -> “FINE_ARTS” “I never declared a major” -> “NEVER_MAJOR”
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Computer science, computer engineering, or software engineering"] <- 'COMPUTER_SCIENCE'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Web development or web design"] <- 'WEB_DEVELOPMENT'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Information systems, information technology, or system administration"] <- 'INFORMATION_SYSTEMS'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Mathematics or statistics"] <- 'MATHS_STATS'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Another engineering discipline (such as civil, electrical, mechanical, etc.)"] <- 'ANOTHER_ENGINEERING_DISCIPLINE'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="A business discipline (such as accounting, finance, marketing, etc.)"] <- 'BUSINESS'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="A health science (such as nursing, pharmacy, radiology, etc.)"] <- 'HEALTH'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="A humanities discipline (such as literature, history, philosophy, etc.)"] <- 'HUMANITIES'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="A natural science (such as biology, chemistry, physics, etc.)"] <- 'NATURAL_SCIENCE'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="A social science (such as anthropology, psychology, political science, etc.)"] <- 'SOCIAL_SCIENCE'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="Fine arts or performing arts (such as graphic design, music, studio art, etc.)"] <- 'FINE_ARTS'
# Reasigamos el valor
data_wo_na$UndergradMajor[data_wo_na$UndergradMajor=="I never declared a major"] <- 'NEVER_MAJOR'
table(as.factor(data_wo_na$UndergradMajor))
plot(as.factor(data_wo_na$UndergradMajor))
boxplot(as.numeric(data_wo_na$Age), main="Valores para Age", col="gray")
atipicos_age <- boxplot.stats(as.numeric(data_wo_na$Age))
head(sort(atipicos_age$out, decreasing = TRUE), n=10)
tail(sort(atipicos_age$out, decreasing = TRUE), n=10)
Al ordenar los valores y analizarlos encontramos que los valores máximos que podrían considerarse outliers son los mayores o iguales a 96 y los menores o iguales a 1.
Por lo tanto, procedemos a borrar esas observaciones que los contienen
data_wo_na <- data_wo_na[data_wo_na$Age < 96, ]
data_wo_na <- data_wo_na[data_wo_na$Age > 1, ]
# volvemos a revisar los valores outliers
boxplot(as.numeric(data_wo_na$Age), main="Valores para Age", col="gray")
A pesar de encontrarse unos valores superiores, por fuera del tercer cuartil (75%) aún se consideran válidos.
boxplot(as.numeric(data_wo_na$ConvertedComp), main="Valores para Sueldo Anual (ConvertedComp)", col="gray")
atipicos_sueldo_anual <- boxplot.stats(as.numeric(data_wo_na$ConvertedComp))
head(sort(atipicos_sueldo_anual$out, decreasing = TRUE), n=10)
tail(sort(atipicos_sueldo_anual$out, decreasing = TRUE), n=10)
Al ordenar los valores y analizarlos encontramos que los valores máximos que podrían considerarse outliers son los mayores o iguales a 208000. Sin embargo, para efectos del análisis vamos a considerar como valores a los no sobrepasan el límite de 500K. Por lo tanto, procedemos a borrar esas observaciones que contienen los valores superiores a el límite.
data_wo_na <- data_wo_na[data_wo_na$ConvertedComp <= 500000, ]
# volvemos a revisar los valores outliers
boxplot(as.numeric(data_wo_na$ConvertedComp), main="Valores para Sueldo Anual (ConvertedComp)", col="gray")
A pesar de encontrarse unos valores superiores, por fuera del tercer cuartil (75%) aún se consideran válidos.
boxplot(as.numeric(data_wo_na$WorkWeekHrs), main="Valores para Horas trabajadas en la semana (WorkWeekHrs)", col="gray")
atipicos_horas_semanales <- boxplot.stats(as.numeric(data_wo_na$WorkWeekHrs))
head(sort(atipicos_horas_semanales$out, decreasing = FALSE), n=10)
tail(sort(atipicos_horas_semanales$out, decreasing = FALSE), n=10)
Al ordenar los valores y analizarlos encontramos que los valores máximos que podrían considerarse outliers son los mayores a 112 horas, ya que trabajar más de 16 horas diarias se encuentra poco probable. De la misma manera, se considera poco probable trabajar menos de una hora diaria (7 horas semanales, incluso en un trabajo a tiempo parcial). Por lo tanto, procedemos a borrar esas observaciones que contienen los valores superiores e inferiores a los límites mencionados.
Cabe recalcar que el valor 112 se obtiene en el supuesto caso que se trabaje 16 horas diarias los 7 dias de la semana, en el caso extremo.
data_wo_na <- data_wo_na[data_wo_na$WorkWeekHrs <= 112, ]
data_wo_na <- data_wo_na[data_wo_na$WorkWeekHrs >= 7, ]
# volvemos a revisar los valores outliers
boxplot(as.numeric(data_wo_na$WorkWeekHrs), main="Valores para Horas trabajadas en la semana (WorkWeekHrs)", col="gray")
A pesar de encontrarse unos valores superiores e inferiores, por fuera del tercer(75%) y primer (25%) cuartil, respectivamente; aún se consideran válidos para efectos del análisis que realizaremos.
boxplot(as.numeric(data_wo_na$YearsCodePro), main="Valores para Años Profesionales (YearsCodePro)", col="gray")
atipicos_years_pro <- boxplot.stats(as.numeric(data_wo_na$YearsCodePro))
head(sort(atipicos_years_pro$out, decreasing = TRUE), n=10)
tail(sort(atipicos_years_pro$out, decreasing = TRUE), n=10)
Al ordenar los valores outliers de manera descendente, encontramos los valores en el rango entre 26 y 50 años. Sin embargo, yendo a la lógica actual en los años que podría tener un desarrollador no encontramos como valor atípico una experiencia comprendida entre los mencionados límites. Para verificar de manera más detallada esta variable, vamos a comparar la variable Age vs. la variable YearsCodePro para descartar alguna anormalidad, como que los años de profesional sean mayor a la edad actual o no haya lógica entre la una con la otra.
Para esto vamos a proceder a generar un data frame entre las 2 variables considerando los valores comprendidos en el rango antes mencionado.
data_age_years_pro <- data_wo_na[data_wo_na$YearsCodePro >= 26, c(1, 12)]
# Obtenemos una variable que almacene la diferencia en la edad como profesional y la edad actual
filas_ndf <- dim(data_age_years_pro)[1]
data_age_years_pro$diferencia <- 0
for(i in 1:filas_ndf) {
data_age_years_pro$diferencia[i] <- data_age_years_pro$Age[i] - data_age_years_pro$YearsCodePro[i]
}
head(sort(data_age_years_pro$diferencia, decreasing = TRUE), n=10)
tail(sort(data_age_years_pro$diferencia, decreasing = TRUE), n=10)
Todos los valores en las diferencias obtenidas son lógicos. Sin embargo, a pesar de encontrarse una diferencia mínima de 8 años entre la experiencia como profesional y la edad actual, sabemos de casos extremos de niños que pueden aprender a programar o cualquier otra habilidad a corta edad, por ende no eliminaremos ningún registro.
Se seleccionan las variables que se usarán de nuestro dataset las cuales serán las que darán respuestas a nuestros cuestionamientos iniciales. Alguna de estas puede no ser relevantes en los siguientes análisis ya que pueden no tener afectación en las respuestas que se buscan.
#Discretizamos por Age
data_wo_na$nivel_edad <- cut(data_wo_na$Age, breaks = c(0,18,35,65,100), labels = c("Niño", "Joven", "Adulto", "Adulto M"))
plot(as.factor(data_wo_na$nivel_edad))
# Tamaño de nuestro dataset y nombre de las columnas
dim(data_wo_na)
names(data_wo_na)
#Resumen de los valores
summary(data_wo_na)
Para este apartado trabajaremos con un análisis visual mediante graficas que permiten observar la similitud entre las distribuciones de los datos y una distribución normal ideal, además realizaremos tests de Shapiro-Wilk para revalidar la normalidad de las variables, asumiremos como hipótesis nula que la población está distribuida normalmente, si el p-valor es menor al nivel de significancia 𝛼 = 0.05.
#Grafica ConvertedComp
qqnorm(data_wo_na$ConvertedComp)
qqline(data_wo_na$ConvertedComp,col=2)
#Grafica Age
qqnorm(data_wo_na$Age)
qqline(data_wo_na$Age,col=2)
#Grafica YearsCodePro
qqnorm(data_wo_na$YearsCodePro)
qqline(data_wo_na$YearsCodePro,col=2)
#Grafica WorkWeekHrs
qqnorm(data_wo_na$WorkWeekHrs)
qqline(data_wo_na$WorkWeekHrs,col=2)
#Grafica db_techs
qqnorm(data_wo_na$db_techs)
qqline(data_wo_na$db_techs,col=2)
#Grafica prog_langs
qqnorm(data_wo_na$prog_langs)
qqline(data_wo_na$prog_langs,col=2)
#Grafica misc_techs
qqnorm(data_wo_na$misc_techs)
qqline(data_wo_na$misc_techs,col=2)
#Grafica collab_techs
qqnorm(data_wo_na$collab_techs)
qqline(data_wo_na$collab_techs,col=2)
#Grafica plat_techs
qqnorm(data_wo_na$plat_techs)
qqline(data_wo_na$plat_techs,col=2)
#Grafica web_techs
qqnorm(data_wo_na$web_techs)
qqline(data_wo_na$web_techs,col=2)
Podemos observar en los histogramas que ninguno de los atributos se asemeja a una normal, el más cercano es el campo collab_techs.
Ahora comprobaremos mediante el test de shapiro si nuestra observación es correcta.
#Realizamos una muestra de 5.000 registros, esto para aplicar el test de shapiro
n<-5000
muestra<- sample(1:nrow(data_wo_na),size=n,replace=FALSE)
data_wo_na_muestra<- data_wo_na[muestra, ]
#Test ConvertedComp
shapiro.test(data_wo_na_muestra$ConvertedComp)
#Test Age
shapiro.test(data_wo_na_muestra$Age)
#Test YearsCodePro
shapiro.test(data_wo_na_muestra$YearsCodePro)
#Test WorkWeekHrs
shapiro.test(data_wo_na_muestra$WorkWeekHrs)
#Test db_techs
shapiro.test(data_wo_na_muestra$db_techs)
#Test prog_langs
shapiro.test(data_wo_na_muestra$prog_langs)
#Test misc_techs
shapiro.test(data_wo_na_muestra$misc_techs)
#Test collab_techs
shapiro.test(data_wo_na_muestra$collab_techs)
#Test plat_techs
shapiro.test(data_wo_na_muestra$plat_techs)
#Test web_techs
shapiro.test(data_wo_na_muestra$web_techs)
Conclusión:
Mediante este test podemos concluir que ningun atributo sigue una distribucion normal.
Para comprobar la homocedasticidad (igualdad de varianzas entre los grupos que se van a comparar) se pueden realizar dos tipos de test Levene y Fligner-Killeen, en nuestro caso como los datos no siguen una distribución normal usaremos Fligner-Killeen.
Compararemos la varianza de ConvertedComp entre los grupos de nivel_edad, EdLevel, Employment y NEWOvertime.
#Homocedasticidad
fligner.test(ConvertedComp ~ nivel_edad, data = data_wo_na)
fligner.test(ConvertedComp ~ EdLevel, data = data_wo_na)
fligner.test(ConvertedComp ~ Employment, data = data_wo_na)
fligner.test(ConvertedComp ~ NEWOvertime, data = data_wo_na)
Conclusión:
Dado que en todos los casos el valor de p-value es inferior a 0.05 se no se acepta la hipótesis de que las varianzas son homogéneas.
En este caso queremos conocer si el número de lenguajes de programación que maneja un programador es similar en las situaciones laborales que se puedan tener (FULL_TIME, PART_TIME, FREELANCER, etc.).
Para esto usaremos la prueba de Kruskal-Wallis, que es un método no paramétrico que busca probar si un grupo de datos proviene de una misma población.
#Prueba de Kruskal-Wallis entre prog_langs y Employment
kruskal.test(prog_langs ~ Employment, data = data_wo_na)
#Representación visual
boxplot(data_wo_na$prog_langs~data_wo_na$Employment, ylab="IGF-I",xlab="Employment")
El valor de p-value obtenido es menor al nivel de significancia se puede concluir que el número de lenguajes que maneja un programador dado su tipo de trabajo es diferente dependiendo de los tipos de situaciones laborales que se estén.
En nuestra búsqueda de conocer cómo se relacionan los distintos atributos del dataset buscaremos la correlación que existe entre estos campos, dado que nuestros valores son no paramétricos, utilizaremos la correlación de Spearman.
#Correlación ConvertedComp-Age
cor.test(data_wo_na$ConvertedComp,data_wo_na$Age, method="spearman")
#Correlación ConvertedComp-YearsCodePro
cor.test(data_wo_na$ConvertedComp,data_wo_na$YearsCodePro, method="spearman")
#Correlación ConvertedComp-WorkWeekHrs
cor.test(data_wo_na$ConvertedComp,data_wo_na$WorkWeekHrs, method="spearman")
#Correlación ConvertedComp-db_techs
cor.test(data_wo_na$ConvertedComp,data_wo_na$db_techs, method="spearman")
#Correlación ConvertedComp-prog_langs
cor.test(data_wo_na$ConvertedComp,data_wo_na$prog_langs, method="spearman")
#Correlación ConvertedComp-misc_techs
cor.test(data_wo_na$ConvertedComp,data_wo_na$misc_techs, method="spearman")
#Correlación ConvertedComp-collab_techs
cor.test(data_wo_na$ConvertedComp,data_wo_na$collab_techs, method="spearman")
#Correlación ConvertedComp-plat_techs
cor.test(data_wo_na$ConvertedComp,data_wo_na$plat_techs, method="spearman")
#Correlación ConvertedComp-web_techs
cor.test(data_wo_na$ConvertedComp,data_wo_na$web_techs, method="spearman")
#Grafica ConvertedComp-Age
plot(data_wo_na$ConvertedComp, data_wo_na$Age, pch = 19, col = "lightblue")
abline(lm(data_wo_na$Age ~ data_wo_na$ConvertedComp), col = "red", lwd = 3)
text(paste("Correlación:", round(cor(data_wo_na$ConvertedComp, data_wo_na$Age, method="spearman"), 2)), x = 25, y = 95)
#Grafica ConvertedComp-YearsCodePro
plot(data_wo_na$ConvertedComp, data_wo_na$YearsCodePro, pch = 19, col = "lightblue")
abline(lm(data_wo_na$YearsCodePro ~ data_wo_na$ConvertedComp), col = "red", lwd = 3)
text(paste("Correlación:", round(cor(data_wo_na$ConvertedComp, data_wo_na$YearsCodePro, method="spearman"), 2)), x = 25, y = 95)
#Grafica ConvertedComp-WorkWeekHrs
plot(data_wo_na$ConvertedComp, data_wo_na$WorkWeekHrs, pch = 19, col = "lightblue")
abline(lm(data_wo_na$WorkWeekHrs ~ data_wo_na$ConvertedComp), col = "red", lwd = 3)
text(paste("Correlación:", round(cor(data_wo_na$ConvertedComp, data_wo_na$WorkWeekHrs, method="spearman"), 2)), x = 25, y = 95)
Conclusión:
Vemos que los atributos de Age y YearsCodePro obtuvieron las correlaciones más grandes respecto al salario (0.45-0.46) y los atributos WorkWeekHrs-db_techs fueron los que menos correlación tienen, esto nos da a entender el número de horas trabajadas no explica tanto el salario de los programadores, al igual que el número de tecnologías que manejan.
En el paso anterior hemos conocido los campos que están más relacionados, ahora usaremos un método de regresión para poder conocer el salario (ConvertedComp) de los programadores a través de otras variables.
Atributos cuantitativos
- YearsCodePro
- Age
- WorkWeekHrs
Atributos cualitativos
- Country
- EdLevel
- Employment
Atributo a predecir
- ConvertedCom
#Seleccionamos los atriburos a usar
data_wo_na_select<-data_wo_na[,c("ConvertedComp","YearsCodePro", "Age", "WorkWeekHrs","Country", "EdLevel", "Employment")]
#Regresión lineal
m1 = lm(ConvertedComp ~ YearsCodePro,data=data_wo_na_select)
m2 = lm(ConvertedComp ~ Age,data=data_wo_na_select)
m3 = lm(ConvertedComp ~ WorkWeekHrs,data=data_wo_na_select)
#Regresión lineal multiple
m4 = lm(ConvertedComp ~ YearsCodePro + Country,data=data_wo_na_select)
m5 = lm(ConvertedComp ~ Age + EdLevel,data=data_wo_na_select)
m6 = lm(ConvertedComp ~ WorkWeekHrs + Employment,data=data_wo_na_select)
m7 <- lm(ConvertedComp ~ ., data = data_wo_na_select)
#Coeficientes de determinación
tabla.coeficientes <- matrix(c(1, summary(m1)$r.squared,
2, summary(m2)$r.squared,
3, summary(m3)$r.squared,
4, summary(m4)$r.squared,
5, summary(m5)$r.squared,
6, summary(m6)$r.squared,
7,summary(m7)$r.squared),
ncol = 2, byrow = TRUE)
colnames(tabla.coeficientes) <- c("Modelo", "R^2")
tabla.coeficientes
#Grafica modelo
qqnorm(m4$residuals)
qqline(m4$residuals)
Conclusión:
Vemos que los coeficientes no han salido tan altos, en este caso los modelos 4 y 7 son los que han dado mejores resultados con 44% de capacidad de predicción, en este caso nos quedamos con el modelo 4 ya que el 7 contiene todos los atributos y no estaríamos reduciendo entradas.
Se aplican 2 test con el modelo 4 para ver su funcionalidad.
#Test usando el modelo 4
test_1 <- data.frame(
YearsCodePro = 10,
Country = "United States"
)
# Predecir salario
predict(m4, test_1)
test_2 <- data.frame(
YearsCodePro = 10,
Country = "France"
)
# Predecir salario
predict(m4, test_2)
Dados los resultados obtenidos en las pruebas que hemos realizado los resultados no han sido del todo satisfactorio ya que hemos encontrado una baja correlación entre los atributos de nuestro dataset lo que ha limitado la capacidad de predicción de los modelos de regresión lineal simple y múltiple, obteniendo solo un 44% de capacidad de acierto en la predicción de los salarios bajo los atributos de YearsCodePro y Country, por otra parte, el ejercicio de comparación entre grupos nos dice que el numero de lenguajes de programación aprendidos va a depender de la situación laboral, lo que tiene sentido.
Siguiendo esta misma línea, los resultados no son extremos, sino que igual se ve una coloración en los trabajos full time y freelancer, lo que da a entender que estas personas tienen mayor experiencia que un part time.
El salario depende en gran medida de muchas variables técnicas y socio demográficas. Al tener encuestados de varios países, donde las condiciones socio demográficas son muy distintas entre ellos, sin duda influye en el desarrollo de habilidades técnicas que garanticen un salario adecuado. Quizás al realizar un análisis más específico a los encuestados de un país determinado se mejore la capacidad predictiva en los salarios.
Quizás al tener una variable donde se detalle el nivel del idioma inglés que domine el encuestado se hubiera podido realizar una análisis más específico. También es importante reconocer que la comunidad de StackOverflow no solo se compone de desarrolladores, sino de un conjunto variado de perfiles técnicos que a diario usan el sistema de preguntas/respuestas. Algunos perfiles enfocados más a la investigación o ciencia pueden verse beneficiados al tener salarios más altos que un simple programador.
Los resultados de nuestros modelos quizás no han sido tan buenos, pero nos ha permitido visualizar la importancia de que la información que estemos usando como fuente tenga la menor cantidad de valores ausentes posibles, ya que con los tratamientos de integración y limpieza se puede mejorar estos resultados de forma estratégica, pero siempre nos estarán alejando de resultados fundamentados en los datos de origen.
El código fuente y el dataset usado para el análisis presentado en este documento se encuentra disponible en el siguiente repositorio de GitHub: https://github.com/gpbonillas/stackoverflow2020
Para ver las gráficas resultantes de los comandos respectivos en cada punto de la Wiki, por favor revisar los documentos PDF o HTML del proyecto.
Contribuciones | Firma |
---|---|
Investigación previa | Luis Arnaldo Torres González, Gabriel Patricio Bonilla Sanchez |
Redacción de las respuestas | Luis Arnaldo Torres González, Gabriel Patricio Bonilla Sanchez |
Desarrollo código | Luis Arnaldo Torres González, Gabriel Patricio Bonilla Sanchez |