Skip to content

Instantly share code, notes, and snippets.

@meraioth
Created October 11, 2023 19:28
Show Gist options
  • Save meraioth/7a4562b466067541853f5d886d538b05 to your computer and use it in GitHub Desktop.
Save meraioth/7a4562b466067541853f5d886d538b05 to your computer and use it in GitHub Desktop.
analisis deuda tecnica.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"name": "analisis deuda tecnica.ipynb",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/meraioth/7a4562b466067541853f5d886d538b05/-an-lisis-deuda-t-cnica.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"# Imports"
],
"metadata": {
"id": "L9gqMsawoRBX"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "uXOPMnKJVAVI"
},
"outputs": [],
"source": [
"# imports\n",
"import requests\n",
"import pandas as pd\n",
"from datetime import date\n",
"from dateutil.relativedelta import relativedelta\n",
"from google.colab import drive\n",
"import logging\n",
"import json\n",
"import re\n",
"import matplotlib.pyplot as plt\n",
"!pip install circlify --quiet\n",
"import circlify\n",
"!pip install colour --quiet\n",
"from colour import Color\n",
"import warnings\n"
]
},
{
"cell_type": "markdown",
"source": [
"# Ownership"
],
"metadata": {
"id": "xq5ByMC1rueE"
}
},
{
"cell_type": "markdown",
"source": [
"En Buk tenemos un equipo owner sobre cada archivo, en nuestro caso este comando nos entrega un listado de archivos del equipo rmcl (Remuneraciones Chile), de aca solo queremos analizar los archivos de la carpeta app de nuestro monolito en Rails\n",
"\n",
"\n",
"```\n",
"rake ownership:details | grep rmcl | awk -F':' '{print $4}' | grep app > team_files.csv\n",
"```\n",
"\n"
],
"metadata": {
"id": "xfRAKIpX6BeB"
}
},
{
"cell_type": "code",
"source": [
"# Leer listado de archivos a analizar\n",
"team_files = pd.read_csv('team_files.csv', header=None, names=['entity'])"
],
"metadata": {
"id": "_7YZr6ST6lci"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Sentry\n",
"Cada evento de sentry tiene una traza del error, buscamos en esa traza el primer archivo que haga match con nuestro team_files y contamos la cantidad de errores por archivo"
],
"metadata": {
"id": "iSGbcK45VgPs"
}
},
{
"cell_type": "code",
"source": [
"SENTRY_API_BASE_URL = 'Replace with your Sentry API base URL'\n",
"SENTRY_AUTH_TOKEN = 'Replace with your Sentry authentication token'\n",
"PROJECT_ID = 'Replace with your Project Id'\n",
"ENVIRONMENT = 'Replace with your enviroment'\n",
"ORGANIZATION = 'Replace with your organization'\n",
"END_DATE = date.today().strftime(\"%Y-%m-%d\")\n",
"START_DATE = (date.today() - relativedelta(years=1)).strftime(\"%Y-%m-%d\")\n",
"PAGES = 100 # PAGES * 100 = #records\n",
"\n",
"def get_events_stack_files():\n",
" data = []\n",
" url = f\"{SENTRY_API_BASE_URL}/organizations/{ORGANIZATION}/events/?query=(event.type:error AND has:stack.filename)&field=stack.filename&field=count()&field=transaction&project={PROJECT_ID}&start={START_DATE}&end={END_DATE}&sort=-count()&environment={ENVIRONMENT}\"\n",
" headers = {\n",
" 'Authorization': f'Bearer {SENTRY_AUTH_TOKEN}',\n",
" 'Content-Type': 'application/json',\n",
" }\n",
" response = requests.get(url, headers=headers)\n",
" if response.status_code == 200:\n",
" data+=(response.json()['data'])\n",
" else:\n",
" print(f\"Failed to retrieve file statistics. Status code: {response.status_code}\")\n",
" return None\n",
"\t\t# Obtenemos la primera pagina para tener los links de las siguientes\n",
" for i in range(PAGES-1):\n",
" response = requests.get(response.headers['link'].split(';')[3].split(',')[-1].strip()[1:-1], headers=headers)\n",
" data+=(response.json()['data'])\n",
" return data\n",
"\n",
"stack_files_response = get_events_stack_files()\n",
"stack_files = pd.DataFrame.from_dict(stack_files_response)"
],
"metadata": {
"id": "11zDVtydVlLy"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"stack_files = stack_files.reset_index()"
],
"metadata": {
"id": "6bahOwh0zcc2"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sentry_stats = pd.DataFrame({'stack.filename': stack_files['stack.filename'].explode(), 'index': stack_files['index'] ,'sentry_errors': stack_files['count()'], 'transaction': stack_files['transaction']})\n",
"sentry_stats = sentry_stats.merge(team_files, left_on='stack.filename', right_on='entity', how='inner')\n",
"sentry_stats = (sentry_stats.reset_index()\n",
" .drop_duplicates(subset=['stack.filename', 'index'], keep='last'))"
],
"metadata": {
"id": "nOG_s5bwBfFd"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sentry_stats = sentry_stats[['entity', 'sentry_errors']]\n",
"sentry_stats = sentry_stats.groupby('entity').agg('sum')"
],
"metadata": {
"id": "erxzgsxEzWnx"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Sonar issues"
],
"metadata": {
"id": "mlsz-_Yu8jrL"
}
},
{
"cell_type": "code",
"source": [
"# documentación de la API y las urls en https://sonarcloud.io/web_api/\n",
"ISSUES_URL =\n",
"TOKEN =\n",
"\n",
"# agregar la autentificación\n",
"session = requests.Session()\n",
"session.auth = TOKEN, ''\n",
"\n",
"\n",
"\n",
"# función para obtener la data de las issues\n",
"def fetch_data(url, session, params):\n",
" res = session.get(url, params=params)\n",
" json_string = res.content.decode('utf-8')\n",
" return json.loads(json_string)\n",
"\n",
"\n",
"# función para definir la cantidad de páginas\n",
"def pages_quantity(url, session, params, ps):\n",
" params['p']= 1\n",
" data = fetch_data(url, session, params)\n",
" try:\n",
" total = data['total']\n",
" except:\n",
" total = data['paging']['total']\n",
" return (total // ps) + 1\n",
"\n",
"\n",
"# juntar todas las issues de todas las páginas en un dataframe\n",
"\n",
"keys_of_interest = ['component', 'severity', 'type', 'message']\n",
"\n",
"data_list = []\n",
"n_pages = pages_quantity(ISSUES_URL, session, {}, 500)\n",
"for page in range(1, n_pages + 1):\n",
" page_data = fetch_data(ISSUES_URL, session, {'p': page, 'ps': 500})\n",
" data_list.extend(page_data.get('issues', []))\n",
"\n",
"filtered_data_list = [{key: dct[key] for key in keys_of_interest} for dct in data_list]\n",
"\n",
"sonar_stats = pd.DataFrame(filtered_data_list)\n",
"\n",
"# limpiar los nombres de los archivos\n",
"sonar_stats['component'] = sonar_stats['component'].apply(lambda x: x.split(':')[1] )\n",
"\n",
"# filtrar los issues según los archivos de rmcl\n",
"sonar_stats = sonar_stats[sonar_stats['component'].isin(team_files['entity'])]"
],
"metadata": {
"id": "Yvmxbdfn8mSB"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# contar los todos agrupados por archivo\n",
"sonar_todos = sonar_stats[sonar_stats['message'] == 'Complete the task associated to this TODO comment.']\n",
"todos_stats = sonar_todos.groupby('component').size().reset_index(name='todos').rename(columns={\"component\": \"entity\"})\n",
"# todos_count.to_csv(dataset_dir+\"data/results/filtered_todos.csv\", index=False)"
],
"metadata": {
"id": "3alRbFuk-EhQ"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# contar los code smells agrupados por archivo\n",
"sonar_code_smells = sonar_stats[sonar_stats['type'] == 'CODE_SMELL'][sonar_stats['message'] != 'Complete the task associated to this TODO comment.']\n",
"code_smells_stats = sonar_code_smells.groupby('component').size().reset_index(name='code_smells').rename(columns={\"component\": \"entity\"})"
],
"metadata": {
"id": "OU_v_fUjQWm1"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Sonar metrics\n",
"La metricas disponibles y sus definiciones se pueden ver en https://docs.sonarcloud.io/digging-deeper/metric-definitions/"
],
"metadata": {
"id": "OGRZPoZVTign"
}
},
{
"cell_type": "code",
"source": [
"MEASURES_URL = 'https://sonarcloud.io/api/measures/component_tree'\n",
"METRICS = ['complexity','cognitive_complexity','duplicated_lines_density', 'ncloc']\n",
"measures = fetch_data(MEASURES_URL, session, {'component': 'your-key-project', 'metricKeys': (',').join(METRICS),'qualifiers':'FIL','ps':'1'})"
],
"metadata": {
"id": "nG9dkfHdTn7U"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"data_list = []\n",
"n_pages = pages_quantity(MEASURES_URL, session, {'component': 'your-key-project', 'metricKeys': (',').join(METRICS),'qualifiers':'FIL'}, 500 )\n",
"for page in range(1, n_pages + 1):\n",
" page_data = fetch_data(MEASURES_URL, session, {'component': 'your-key-project', 'metricKeys': (',').join(METRICS),'qualifiers':'FIL','p': page, 'ps': 500})\n",
" data_list.extend(page_data['components'])"
],
"metadata": {
"id": "inXZ24JlUAQN"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sonar_metrics = pd.DataFrame(data_list)\n",
"sonar_metrics = sonar_metrics.merge(team_files, left_on='path', right_on='entity', how='inner')"
],
"metadata": {
"id": "FjxAyn30hj0i"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sonar_metrics"
],
"metadata": {
"id": "yGXkfh8x3810"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sonar_metrics = sonar_metrics[['entity', 'measures']]\n",
"sonar_metrics = sonar_metrics.explode('measures')"
],
"metadata": {
"id": "vSpuNIZklalp"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"sonar_metrics"
],
"metadata": {
"id": "9NLD3VQsBYew"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"metric_keys= ['bestValue', 'metric', 'value']\n",
"for key in metric_keys:\n",
" sonar_metrics[key] = sonar_metrics['measures'].apply(lambda x: x.get(key))\n",
"\n",
"sonar_metrics.drop(columns=['measures'], inplace=True)"
],
"metadata": {
"id": "7OVLEgQkBNlV"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"filtered_sonar_metric = []\n",
"for metric in METRICS:\n",
" filtered = sonar_metrics[sonar_metrics['metric'] == metric]\n",
" filtered_sonar_metric.append(filtered.rename(columns={'value': metric}))\n"
],
"metadata": {
"id": "Rjy6Y7ltDwUX"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"grouped_sonar_metric = filtered_sonar_metric[0]\n",
"for i in range(1, len(filtered_sonar_metric)):\n",
" grouped_sonar_metric = grouped_sonar_metric.merge(filtered_sonar_metric[i], on='entity', how='outer')\n"
],
"metadata": {
"id": "j_DyTOUPFU2Z"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"metric_stats = grouped_sonar_metric[['entity']+METRICS]"
],
"metadata": {
"id": "-ItVQWy8DsEB"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Coverage\n",
"\n",
"En nuestro caso en particular obtenemos el coverage desde los pipelines de CI/CD\n",
"\n"
],
"metadata": {
"id": "Q-WjdAFvrrur"
}
},
{
"cell_type": "code",
"source": [
"COVERAGE_FILE_PATH = \"coverage.json\"\n",
"\n",
"def read_coverage_file():\n",
" # Depende como obtengas tu coverage debes reescribir esta función\n",
" return None\n",
"\n",
"coverage = read_coverage_file()\n",
"coverage_stats = coverage.merge(team_files, on='entity', how='inner')\n",
"coverage_stats['inverse_coverage'] = 100 - coverage_stats['coverage']\n"
],
"metadata": {
"id": "NN_VMt3IYmIg"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"coverage_stats"
],
"metadata": {
"id": "u-UPp61RGLdP"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Code Maat (Github info)"
],
"metadata": {
"id": "Tw37SnpY4Cep"
}
},
{
"cell_type": "markdown",
"source": [
"Lo primero que hay hacer es descargar el registro de cambios de git, esto corriendo lo siguiente dentro de la carpeta de tu proyecto:\n",
"\n",
"\n",
"```\n",
"git log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN' --no-renames --after=2021-06-01 > logfile.log\n",
"```\n",
"Con esto descargamos todos los commits que han sido creados desde el 1 de junio del 2022 y se guardan en el archivo logfile.log\n",
"\n",
"A continuación es necesario clonar el [repositorio](https://github.com/adamtornhill/code-maat) de code-maat, agregar el archivo recien creado (en el root) y proceder a generar el análisis.\n",
"\n",
"## Intrucciones para correrlo con docker:\n",
"\n",
"Paso 1: Construir la imagen de Docker\n",
"Dependiendo del computador les podría tocar hacer cambios al dockerfile (para mac con m1 hay que cambiar la primera linea por `FROM clojure:latest`) pueden revisar las issues del repositorio si es que tienen problemas.\n",
"\n",
"```\n",
" docker build -t code-maat-app .\n",
"```\n",
"\n",
"Paso 2: Correr el comando en un nuevo contenedor para la imagen creada y guardar los resutador en un archivo.\n",
"Una vez creado el contenedor, se pueden correr los comandos con `docker exec` en vez de `docker run` para no levantar otro contenedor.\n",
"\n",
"```\n",
" docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -c git2 -a age > entity-age.csv\n",
"```\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"\n"
],
"metadata": {
"id": "xoGVQm97ruXP"
}
},
{
"cell_type": "code",
"source": [
"for i in range(9):\n",
" zero_date = (date.today() - relativedelta(months=i)).strftime(\"%Y-%m-%d\")\n",
" print(f'docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d {zero_date} -c git2 -a age > entity-age-{i}.csv')"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "tnne2ayhbFMm",
"outputId": "0738e33b-1003-4529-f12c-626444875be3"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-07-18 -c git2 -a age > entity-age-0.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-06-18 -c git2 -a age > entity-age-1.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-05-18 -c git2 -a age > entity-age-2.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-04-18 -c git2 -a age > entity-age-3.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-03-18 -c git2 -a age > entity-age-4.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-02-18 -c git2 -a age > entity-age-5.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2023-01-18 -c git2 -a age > entity-age-6.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2022-12-18 -c git2 -a age > entity-age-7.csv\n",
"docker run -v \"$PWD\":/data -it code-maat-app -l /data/logfile.log -d 2022-11-18 -c git2 -a age > entity-age-8.csv\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"ages_stats = team_files\n",
"for i in range(9):\n",
" age_month = pd.read_csv(f\"entity-age-{i}.csv\").rename(columns={'age-months': f'age-{i}'})\n",
" ages_stats = ages_stats.merge(age_month, on='entity', how='inner')\n",
"ages_stats['inverse_avg_age'] = 5 - ages_stats.iloc[:,1:].astype(float).mean(axis=1)\n",
"ages_stats = ages_stats[['entity', 'inverse_avg_age']]"
],
"metadata": {
"id": "ekWjnhMZeSvb"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"entity_churn_stats = pd.read_csv(\"entity-churn.csv\")\n",
"entity_churn_stats = entity_churn_stats.merge(team_files, on='entity', how='inner')[['entity', 'commits']]"
],
"metadata": {
"id": "3ELN_mnOrwvi"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"entity_churn_stats['commits'].sort_values(ascending=False, ignore_index=True).quantile(.8)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "NTWpeyL53C_F",
"outputId": "0753cfbe-82e1-43a5-f7ed-a2623d2c8d8c"
},
"execution_count": null,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"12.0"
]
},
"metadata": {},
"execution_count": 72
}
]
},
{
"cell_type": "code",
"source": [
"entity_churn_stats['commits'].sort_values(ascending=False, ignore_index=True).plot(kind='bar', xticks=[], width=1.0, xlabel='Archivos', ylabel='Commits' )"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 443
},
"id": "PkO7-V-ksA5s",
"outputId": "c62e3df3-d0dd-4864-84dc-734918fb08bd"
},
"execution_count": null,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"<Axes: xlabel='Archivos', ylabel='Commits'>"
]
},
"metadata": {},
"execution_count": 73
},
{
"output_type": "display_data",
"data": {
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
],
"image/png": "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Ho0YMUKrV6/WjTfeGO6yAACXIJyn3jiFZi4jOjuvvfaacnNztXjxYu3fv18jRozQxIkTVVVVFe7SAABAmNksy7LCXUR7jR07VmPGjNHzzz8vSWpqalJycrKys7O1YMGCb3y/z+eT0+lUcs5mRdmv6OhyAQBACHz44/8np9Mpr9crh8PR6rwufxrr66+/VllZmfLy8vxjUVFRcrvdKikpafE99fX1qq+v97/2er2SpKb6rzq2WAAAEDI+n0+S9E19my4fdv7v//5PjY2NSkhICBhPSEjQ4cOHW3xPQUGBlixZ0mz85Jr7O6JEAADQAZJXnv/3zJkzcjqdrc7r8mEnGHl5ecrNzfW/bmpqUnV1teLj42Wz2cJYGQAAaCvLsnTmzBm5XK6LzuvyYedb3/qWunXrpsrKyoDxyspKJSYmtvgeu90uu90eMBYbG9tRJQIAgA5ysY7OBV3+aqwePXpo1KhR2rFjh3+sqalJO3bsUHp6ehgrAwAAkaDLd3YkKTc3VzNmzNDo0aN14403auXKlaqrq9MDDzwQ7tIAAECYGRF27r77bp0+fVqLFi2Sx+PR9ddfr6KiomaLlgEAwOXHiPvsALi8HTt2TKmpqfrggw90/fXXtzhnw4YNysnJUU1NTafWBiD8uvyaHQBdU0lJibp166YpUzrnFv133323Pv744075LACRhbADICzWrVun7OxsFRcX69SpU63OsyxL586da/fnxcTEqF+/fu0+DoCuh7ADoNPV1tbqtdde05w5czRlyhRt2LDBv2/37t2y2Wx66623NGrUKNntdv35z39WU1OTli9frmuuuUZ2u139+/fX008/HXDczz//XOPHj9cVV1yhESNGBNxFfcOGDf5bTHz88cey2WzNbjy6YsUKDRgwwP96z549uvHGG2W325WUlKQFCxYEBK//+Z//0fDhwxUTE6P4+Hi53W7V1dWF8H8KQCgQdgB0us2bN2vw4MEaNGiQ7r33Xr300kvNbve+YMECLV26VOXl5bruuuuUl5enpUuXKj8/X3/961+1adOmZhch/OQnP9Fjjz2mAwcO6Nprr9U999zTYlfo2muv1ejRo7Vx48aA8Y0bN+oHP/iBJOnkyZOaPHmyxowZo4MHD2rNmjVat26dnnrqKUnS3/72N91zzz168MEHVV5ert27d2v69OnfeNt6AGFgAUAnu/nmm62VK1dalmVZDQ0N1re+9S1r165dlmVZ1q5duyxJ1tatW/3zfT6fZbfbrRdffLHF4x09etSSZP3617/2jx06dMiSZJWXl1uWZVnr16+3nE6nf/+KFSusAQMG+F8fOXIkYP6Pf/xja9CgQVZTU5N/TmFhodW7d2+rsbHRKisrsyRZx44da99/BoAOR2cHQKc6cuSI3nvvPd1zzz2SpOjoaN19991at25dwLzRo0f7/y4vL1d9fb0mTJhw0WNfd911/r+TkpIkSVVVVS3OzczM1LFjx7R3715J57s6N9xwgwYPHuz/zPT09IBHyIwbN061tbX64osvNGLECE2YMEHDhw/XnXfeqRdffFFffvllW/8bAHQiwg6ATrVu3TqdO3dOLpdL0dHRio6O1po1a/S73/1OXq/XP69Xr17+v2NiYtp07O7du/v/vhBSmpqaWpybmJioW2+9VZs2bZIkbdq0SVlZWW3+Ht26ddP27dv11ltvaciQIVq9erUGDRqko0ePtvkYADoHYQdApzl37pxefvllPfvsszpw4IB/O3jwoFwul1555ZUW3zdw4EDFxMQEPBYmFLKysvTaa6+ppKREn3/+uTIzM/370tLSVFJSErAG53//93/Vp08fXXXVVZLOB6px48ZpyZIl+uCDD9SjRw9t2bIlpDUCaD/CDoBOs23bNn355ZeaOXOmhg0bFrBlZGQ0O5V1Qc+ePfXkk0/qiSee0Msvv6zPPvtMe/fubXV+W02fPl1nzpzRnDlzNH78+IAnJz/88MOqqKhQdna2Dh8+rN///vdavHixcnNzFRUVpdLSUj3zzDN6//33deLECb3++us6ffq00tLS2lUTgNAz4nERALqGdevWye12t/iU4oyMDC1fvlwffvhhi+/Nz89XdHS0Fi1apFOnTikpKUkPPfRQu+rp06ePpk6dqs2bN+ull14K2HfllVfqj3/8ox5//HGNGDFCcXFxmjlzphYuXChJcjgcKi4u1sqVK+Xz+XT11Vfr2Wef1aRJk9pVE4DQ43ERAADAaJzGAgAARiPsAAAAoxF2AACA0Qg7AADAaIQdAABgNMIOAAAwGmEHAAAYjbADAACMRtgBAABGI+wAAACjEXYAAIDRCDsAAMBo/x/g59sQWZiWmAAAAABJRU5ErkJggg==\n"
},
"metadata": {}
}
]
},
{
"cell_type": "markdown",
"source": [
"# Summary\n",
"\n",
"Mergeamos todas los dataframes anteriores, luego normalizamos y ponderamos para tratar de llevar mucha información a un solo indicador\n"
],
"metadata": {
"id": "SKwFTE7j9j-Z"
}
},
{
"cell_type": "code",
"source": [
"joined_df = sentry_stats.merge(todos_stats,on='entity', how='outer').merge(code_smells_stats,on='entity', how='outer').merge(metric_stats,on='entity', how='outer').merge(coverage_stats,on='entity', how='outer').merge(ages_stats,on='entity', how='outer').merge(entity_churn_stats,on='entity', how='outer')\n",
"filled_df = joined_df.fillna(0)"
],
"metadata": {
"id": "fVWBHUe99tph"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"filled_df"
],
"metadata": {
"id": "bLlcWjHwC8kP"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"s0 = filled_df.iloc[:,1:].astype(float)\n",
"normalized_df = pd.concat([filled_df.iloc[:,:1], (s0 - s0.min()) / (s0.max() - s0.min())], axis=1)\n",
"normalized_df2 = pd.concat([filled_df.iloc[:,:1], (s0 - s0.mean()) / (s0.std())], axis=1)"
],
"metadata": {
"id": "xqiqyAmeCf7I"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"columns_to_average = {'sentry_errors': 5,\n",
" 'todos':5,\n",
" 'code_smells':10,\n",
" 'cognitive_complexity':30,\n",
" 'duplicated_lines_density':10,\n",
" 'ncloc':20,\n",
" 'inverse_coverage':5,\n",
" 'inverse_avg_age':5,\n",
" 'commits':10}\n",
"normalized_df['summary'] = normalized_df.apply(lambda row: sum(row[col] * weight for col, weight in columns_to_average.items()), axis=1)"
],
"metadata": {
"id": "4YuhAMeHGBOH"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"normalized_df.sort_values(by='summary', ascending=False).to_csv(\"normalized_summary.csv\", index=False)"
],
"metadata": {
"id": "hXjVB70Rs1SY"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"normalized_df[['entity','summary']].sort_values(by='summary', ascending=False)"
],
"metadata": {
"id": "y2Ld3gdkDveR"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"corr = normalized_df[columns_to_average.keys()].corr()\n",
"corr.style.background_gradient(cmap='coolwarm')\n"
],
"metadata": {
"id": "wj5c_lWsnvQc"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Function to update the 'datum' recursively\n",
"def update_datum(node):\n",
" if 'children' in node:\n",
" for child in node['children']:\n",
" update_datum(child)\n",
" node['datum'] += child['datum']\n",
"\n",
"# Esta funcion renderea circulos en base a los directorios en comun,\n",
"# asume que df tiene la columna entity, defines que columna ocupas para el tamaño de los circulos\n",
"# y que columna usar para las tonalidades de los circulos\n",
"\n",
"def render_hierarchy(df, column_size='lines_of_code',colum_color='lines_of_code', limit_red_flag=300, title='Repartition of code length for youngest files', operator='<', filter_by_commits=False, n_commits=30):\n",
" colors_red = list(Color(\"yellow\").range_to(Color(\"red\"), df[colum_color].max()))\n",
" colors_grey = list(Color(\"grey\").range_to(Color(\"black\"),10))\n",
" colors_blue = list(Color(\"lightblue\").range_to(Color(\"darkblue\"), df[colum_color].max()))\n",
" colors_background_circle = list(Color(\"lightblue\").range_to(Color(\"darkblue\"),5))\n",
"\n",
" filtered_df = df[df['entity'].str.startswith('app')]\n",
"\n",
" datum = filtered_df[column_size].sum()\n",
"\n",
" json_data = [{'id': 'app', 'datum': datum, 'color': 0, 'children': []}]\n",
"\n",
" for _, row in filtered_df.iterrows():\n",
" path = row['entity'].split('/')[1:-1]\n",
" current_level = json_data[0]['children']\n",
" for folder in path:\n",
" existing_folder = next((child for child in current_level if child['id'] == folder), None)\n",
" if existing_folder:\n",
" current_level = existing_folder['children']\n",
" else:\n",
" new_folder = {'id': folder, 'datum': 0, 'children': [], 'color': 0}\n",
" current_level.append(new_folder)\n",
" current_level = new_folder['children']\n",
"\n",
" leaf_node = {'id': row['entity'].split('/')[-1], 'datum': row[column_size], 'color': row[colum_color],'commits': row['commits']}\n",
" current_level.append(leaf_node)\n",
"\n",
"\n",
"\n",
" update_datum(json_data[0])\n",
"\n",
" circles = circlify.circlify(\n",
" json_data,\n",
" show_enclosure=False,\n",
" target_enclosure=circlify.Circle(x=0, y=0, r=1)\n",
" )\n",
"\n",
" fig, ax = plt.subplots(figsize=(14,14))\n",
"\n",
" ax.set_title(title)\n",
"\n",
" ax.axis('off')\n",
"\n",
" lim = max(\n",
" max(\n",
" abs(circle.x) + circle.r,\n",
" abs(circle.y) + circle.r,\n",
" )\n",
" for circle in circles\n",
" )\n",
" plt.xlim(-lim, lim)\n",
" plt.ylim(-lim, lim)\n",
"\n",
" for circle in circles:\n",
" if circle.level != 2:\n",
" continue\n",
" x, y, r = circle\n",
" ax.add_patch( plt.Circle((x, y), r, alpha=0.5, linewidth=2, color=\"lightblue\"))\n",
"\n",
" for circle in circles:\n",
" if circle.level != 2:\n",
" continue\n",
" x, y, r = circle\n",
" label = circle.ex[\"id\"]\n",
" plt.annotate(label, (x,y+r ) ,va='center', ha='center', fontsize=12, bbox=dict(facecolor='white', edgecolor='black', boxstyle='round', pad=.5))\n",
"\n",
"\n",
" for i in range(3,10):\n",
" for circle in circles:\n",
" if circle.level != i or eval(f\"x {operator} y\", {}, {'x': circle.ex[\"datum\"], 'y': limit_red_flag}):\n",
" continue\n",
" if circle.ex.get(\"children\"):\n",
" x, y, r = circle\n",
" label = circle.ex[\"id\"]\n",
" if len(circle.ex.get(\"children\")) > 1: plt.annotate(label, (x,y+r) , fontsize=13-i, va='top', ha='center', bbox=dict(facecolor='white', edgecolor='black', boxstyle='round', pad=.5, alpha=0.5))\n",
" ax.add_patch( plt.Circle((x, y), r, alpha=0.1, linewidth=2, color=colors_grey[i].hex))\n",
" else:\n",
" x, y, r = circle\n",
" label = circle.ex[\"id\"]\n",
" try:\n",
" if circle.ex[\"commits\"] <= n_commits and filter_by_commits:\n",
" ax.add_patch( plt.Circle((x, y), r, alpha=0.5, linewidth=2, color=colors_blue[circle.ex[\"color\"]-1].hex))\n",
" else:\n",
" ax.add_patch( plt.Circle((x, y), r, alpha=0.5, linewidth=2, color=colors_red[circle.ex[\"color\"]-1].hex))\n",
" continue\n",
" except IndexError:\n",
" print(circle.ex[\"color\"])\n",
" print(df[colum_color].max())"
],
"metadata": {
"id": "NNOrr0k2sJHC"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"warnings.filterwarnings('ignore')\n",
"logging.getLogger().setLevel(logging.ERROR)\n",
"int0 = filled_df.iloc[:,1:].astype(float).astype(int)\n",
"df = pd.concat([filled_df.iloc[:,:1], int0], axis=1)\n",
"for column in ['sentry_errors', 'todos', 'code_smells', 'cognitive_complexity','duplicated_lines_density', 'inverse_coverage']:\n",
" render_hierarchy(df[df[column] > 0][df['ncloc'] > 0], 'ncloc', column, 20, title=f'Repartition of {column}')\n",
" plt.savefig(f'Repartition of {column}.png')\n",
" render_hierarchy(df[df[column] > 0][df['ncloc'] > 0], 'ncloc', column, 20, title=f'Repartition of {column}', filter_by_commits=True)\n",
" plt.savefig(f'Repartition of {column} (filtered).png')\n"
],
"metadata": {
"id": "8-TPw7NH4rZo"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [],
"metadata": {
"id": "0UmPKdObFWrS"
},
"execution_count": null,
"outputs": []
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment