Proyecto final: Dinámica del bosque secundario Florencia, Turrialba, Costa Rica¶


Estudiante: Marcella Sarti Arellano¶


Descripción del proyecto y justificación:¶

El presente proyecto trata sobre la exploración y análisis de un bosque secundario ubicado en la provincia de Cartago, Cantón Turrialba. El bosque se encuentra en la finca privada de el CATIE y tiene una edad aproximada de 30 años.

La base de datos proviene de la investigación a largo plazo del CATIE, en donde busca implementar y dar seguimiento a una red de parcelas permanentes en todo el país. La base de datos a estudiar incluye información de diámetros a la altura del pecho e identificación taxonómica de distintos individuos del bosque.

La importancia del estudio de la ciencia de datos y bosques secundarios radica en que son ecosistemas importantes para la conservación de la biodiversidad, por su potencial para albergar especies de bosques primarios, proveer hábitat para fauna (Chazdon et al. 2009; Turner et al. 1997) y provisión de servicios ecosistémicos (Chokkalingam et al. 2001) por lo que su monitoreo constante es esencial para observar sus dinámicas y asegurarnos que están recuperándose y no degradándose.

La exploración y análisis de la base de datos puede darle un mejor panorama a CATIE de la situación del bosque Florencia, además de proponer otro tipo de mediciones para su mejor comprensión, como por ejemplo, la incorporación de análisis de suelos o mediciones de rasgos funcionales de especies vegetales.

1. Antecendentes¶

Los bosques tropicales son ecosistemas importantes que contribuyen al bienestar de las personas, especialmente en países en vías de desarrollo, ya que proveen servicios ecosistémicos que contribuyen a la seguridad alimentaria, reducción del riesgo de desastres, reducción de enfermedades y oportunidad de generar ingresos (Brandon, 2015; Mullan, 2015). Además, contribuyen a mitigar el cambio climático a través del almacenamiento de más del 25 % del carbono del planeta, siendo los bosques tropicales de Latinoamérica los que almacenan el 49 % de la biomasa de carbono (Brandon, 2015; Saatchi et al., 2011). Sin embargo, la deforestación y degradación de estos ecosistemas se ha intensificado gracias a distintas perturbaciones, afectando la provisión de servicios ecosistémicos (Eguiguren et al., 2019; Malhi et al., 2014). Desde 1990 al 2020, se han deforestado 368 millones de hectáreas de bosques tropicales y se estima que el área de bosques degradados puede ser mayor (FAO, 2020). El monitoreo de los bosques pueden proporcionar información sobre el estado del bosque para los tomadores de decisiones, ya que es posible relacionarlo con la capacidad de adaptación al cambio climático y provisión de servicios ecosistémicos del sitio (Acharya et al., 2011), así como la elección y justificación de medidas de restauración (Putz & Romero, 2014) y pudiendo incorporar a los bosques degradados en mecanismos de REDD+, pago por servicios ambientales y otras estrategias para recuperar servicios ecosistémicos.

El monitoreo de bosques tropicales es objeto de la ciencia de los datos, debido a que se realizan mediciones de atributos cuantitativos como cualitativos en las distintas formas de vida que se encuentran en los bosques a través del tiempo. Por lo que en una unidad muestral, podemos encontrarnos con una cantidad significativa de datos, especialmente en los trópicos, en donde la variación de especies es mucho mayor que en los climas templados y boreales(Wright, 2010). Ganivet & Bloomberg (2019) expresan que los datos de las especies de bosques tanto tomados en campo como con sensores remotos son esenciales para que los tomadores de decisiones puedan implementar planes de manejo o intervención adecuados.

Sin embargo, la recolección y análisis de datos para el monitoreo de los bosques resulta una tarea compleja, la cual implica recursos y habilidades específicas. De acuerdo con los resultados de la investigación de Misiukas et al. (2021), en países tropicales, las capacidades de reporte de datos pueden ser desafiantes aunque la mayoría de países se experimentan importantes impactos antropogénicos hacia los bosques, causando una mayor necesidad de monitorearlos. Adicionalmente, en este estudio, se evidencia que la recolección y análisis de información respecto a los bosques es tan pobre en Centroamérica, que los autores no pudieron ejecutar el análisis en dicha área del trópico.

De ahí, surge la necesidad que la academia en Centroamérica pueda emplear la ciencia de datos para el monitoreo de bosques. Ejemplo de ello son estudios como el de Gerwing (2002) realizado en la cuenca del Amazonas, en donde evaluó atributos de árboles y su relación con la intensidad de distintas perturbaciones como extracción de madera y fuego. Por lo que su investigación analizó datos no solo del bosque sino también del regimen de impacto.

Otro ejemplo del manejo de datos es el del estudio de Longo et al. (2016) también realizado en el Amazonas, en donde unió los datos de 9 inventarios forestales de distintos sitios, con una muestra total de 359 parcelas para estimar la biomasa en bosques con y sin indicios de degradación. Los resultados resaltaron la importancia del monitreo de bosques para conocer las reservas de carbono y su contribución al cambio climático.

Artículos mencionados que emplean a la ciencia de datos para el monitoreo de bosques:

  1. Ganivet, E., & Bloomberg, M. (2019). Towards rapid assessments of tree species diversity and structure in fragmented tropical forests: A review of perspectives offered by remotely-sensed and field-based data. In Forest Ecology and Management (Vol. 432). https://doi.org/10.1016/j.foreco.2018.09.003

  2. Gerwing, J. J. (2002). Degradation of forests through logging and fire in the eastern Brazilian Amazon. Forest Ecology and Management, 157(1–3). https://doi.org/10.1016/S0378-1127(00)00644-7

  3. Longo, M., Keller, M., dos-Santos, M. N., Leitold, V., Pinagé, E. R., Baccini, A., Saatchi, S., Nogueira, E. M., Batistella, M., & Morton, D. C. (2016). Aboveground biomass variability across intact and degraded forests in the Brazilian Amazon. Global Biogeochemical Cycles, 30(11). https://doi.org/10.1002/2016GB005465

  4. Misiukas, J. M., Carter, S., & Herold, M. (2021). Tropical forest monitoring: Challenges and recent progress in research. Remote Sensing, 13(12). https://doi.org/10.3390/rs13122252

2. Descripción del problema y objetivo:¶

CATIE ha realizado una labor invaluable en la restauración y monitoreo de bosques secundarios en Costa Rica, en este caso específico, en el cantón Turrialba, provincia de Cartago, en donde se ha estudiado de manera consecutiva al bosque secundario "Florencia". Sin embargo, después de tomar los datos en campo, nadie se ha dado a la tarea de analizar la estructura de los datos ni tampoco procesarlos para obtener algún tipo de indicador de biodiversidad o estructura del bosque tomando en cuenta la dinámica del bosque a través del tiempo. Únicamente se han procesado datos de la última medición tomada en el año 2021, dejando por un lado al resto de los datos.

Por ello, el objetivo de este proyecto será caracterizar a los datos tomados en las parcelas de monitoreo permanente durante tres años consecutivos en el bosque secundario Florencia. Además de realizar este análisis exploratorio con el paquete Pandas y Pandas profiling, también se corregirán los datos faltantas y se calcularán algunos indicadores sobre la condición del bosque (estructura y biodiversidad) a través del tiempo.

3. Descripción del conjunto de datos:¶

El sitio de estudio es el bosque secundario Florencia, ubicado en Turrialba, Cartago, Costa Rica. El bosque posee más de 30 años desde su regeneración y CATIE ha tomado datos de diámetro a la altura del pecho en milímetros, ubicación e identificación taxonómica de cada individuo dentro de cada una de las 12 parcelas de 0.25 hectáreas establecidas en el sitio de estudio. Las mediciones se realizaron en los años 2018, 2019 y 2020.

La base de datos proporcionada posee más de 8,000 registros y solo poseen una publicación hasta la fecha (Camacho et al., 2021) pero sin tomar en cuenta la dinámica entre las mediciones. Por lo que esta presentación del conjunto de datos mostrará información inédita.

La base de datos posee diversas columnas, de las cuales solo se utilizarán las siguientes:

  • Parcela: número de parcela dentro del sitio evaluado

  • Subparcela: número de subparcela, perteneciente a una determinada parcela, dentro del sitio evaluado.

  • Numero_arbol: número del árbol dentro de la parcela.

  • Genero: genero taxonómico del individuo

  • Especie: especie taxonómica del individuo

  • Forma de vida: tipo de forma de vida del individuo la cual puede ser

  1. Individuo con género y especie desconocida
  2. Árboles
  3. Palmas
  4. Plantas de la familia Musaceae (bananos)
  5. No existen individuos con esta numeración en la base de datos
  6. Hierbas
  7. Árboles
  • Eje: número de eje del árbol. Un árbol puede tener uno o más ejes.
  • Numero_medicion: número de medición del árbol
  1. año 2018
  2. año 2019
  3. año 2020
  • Dap(mm): medición del diámetro a la altura del pecho del eje, correspondiente a cada individuo.

4. Procesamiento y visualización de datos:¶

In [1]:
!pip install pandas
!pip install ydata-profiling
Requirement already satisfied: pandas in /usr/local/lib/python3.10/dist-packages (2.0.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/dist-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas) (2024.1)
Requirement already satisfied: numpy>=1.21.0 in /usr/local/lib/python3.10/dist-packages (from pandas) (1.25.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.8.2->pandas) (1.16.0)
Collecting ydata-profiling
  Downloading ydata_profiling-4.8.3-py2.py3-none-any.whl (359 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 359.5/359.5 kB 7.7 MB/s eta 0:00:00
Requirement already satisfied: scipy<1.14,>=1.4.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.11.4)
Requirement already satisfied: pandas!=1.4.0,<3,>1.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.0.3)
Requirement already satisfied: matplotlib<3.9,>=3.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (3.7.1)
Requirement already satisfied: pydantic>=2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.7.2)
Requirement already satisfied: PyYAML<6.1,>=5.0.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (6.0.1)
Requirement already satisfied: jinja2<3.2,>=2.11.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (3.1.4)
Collecting visions[type_image_path]<0.7.7,>=0.7.5 (from ydata-profiling)
  Downloading visions-0.7.6-py3-none-any.whl (104 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 104.8/104.8 kB 12.2 MB/s eta 0:00:00
Requirement already satisfied: numpy<2,>=1.16.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.25.2)
Collecting htmlmin==0.1.12 (from ydata-profiling)
  Downloading htmlmin-0.1.12.tar.gz (19 kB)
  Preparing metadata (setup.py) ... done
Collecting phik<0.13,>=0.11.1 (from ydata-profiling)
  Downloading phik-0.12.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (686 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 686.1/686.1 kB 41.8 MB/s eta 0:00:00
Requirement already satisfied: requests<3,>=2.24.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.31.0)
Requirement already satisfied: tqdm<5,>=4.48.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (4.66.4)
Requirement already satisfied: seaborn<0.14,>=0.10.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.13.1)
Collecting multimethod<2,>=1.4 (from ydata-profiling)
  Downloading multimethod-1.11.2-py3-none-any.whl (10 kB)
Requirement already satisfied: statsmodels<1,>=0.13.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.14.2)
Collecting typeguard<5,>=3 (from ydata-profiling)
  Downloading typeguard-4.3.0-py3-none-any.whl (35 kB)
Collecting imagehash==4.3.1 (from ydata-profiling)
  Downloading ImageHash-4.3.1-py2.py3-none-any.whl (296 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 296.5/296.5 kB 26.8 MB/s eta 0:00:00
Requirement already satisfied: wordcloud>=1.9.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.9.3)
Collecting dacite>=1.8 (from ydata-profiling)
  Downloading dacite-1.8.1-py3-none-any.whl (14 kB)
Requirement already satisfied: numba<1,>=0.56.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.58.1)
Requirement already satisfied: PyWavelets in /usr/local/lib/python3.10/dist-packages (from imagehash==4.3.1->ydata-profiling) (1.6.0)
Requirement already satisfied: pillow in /usr/local/lib/python3.10/dist-packages (from imagehash==4.3.1->ydata-profiling) (9.4.0)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2<3.2,>=2.11.1->ydata-profiling) (2.1.5)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (1.2.1)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (4.52.4)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (1.4.5)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (24.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (3.1.2)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (2.8.2)
Requirement already satisfied: llvmlite<0.42,>=0.41.0dev0 in /usr/local/lib/python3.10/dist-packages (from numba<1,>=0.56.0->ydata-profiling) (0.41.1)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas!=1.4.0,<3,>1.1->ydata-profiling) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas!=1.4.0,<3,>1.1->ydata-profiling) (2024.1)
Requirement already satisfied: joblib>=0.14.1 in /usr/local/lib/python3.10/dist-packages (from phik<0.13,>=0.11.1->ydata-profiling) (1.4.2)
Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (0.7.0)
Requirement already satisfied: pydantic-core==2.18.3 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (2.18.3)
Requirement already satisfied: typing-extensions>=4.6.1 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (4.12.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (2024.2.2)
Requirement already satisfied: patsy>=0.5.6 in /usr/local/lib/python3.10/dist-packages (from statsmodels<1,>=0.13.2->ydata-profiling) (0.5.6)
Requirement already satisfied: attrs>=19.3.0 in /usr/local/lib/python3.10/dist-packages (from visions[type_image_path]<0.7.7,>=0.7.5->ydata-profiling) (23.2.0)
Requirement already satisfied: networkx>=2.4 in /usr/local/lib/python3.10/dist-packages (from visions[type_image_path]<0.7.7,>=0.7.5->ydata-profiling) (3.3)
Requirement already satisfied: six in /usr/local/lib/python3.10/dist-packages (from patsy>=0.5.6->statsmodels<1,>=0.13.2->ydata-profiling) (1.16.0)
Building wheels for collected packages: htmlmin
  Building wheel for htmlmin (setup.py) ... done
  Created wheel for htmlmin: filename=htmlmin-0.1.12-py3-none-any.whl size=27080 sha256=28b05d0254387fb0f72e8dfdea69043d2bb291fa0d90f1f17ceb07e1c16bfae2
  Stored in directory: /root/.cache/pip/wheels/dd/91/29/a79cecb328d01739e64017b6fb9a1ab9d8cb1853098ec5966d
Successfully built htmlmin
Installing collected packages: htmlmin, typeguard, multimethod, dacite, imagehash, visions, phik, ydata-profiling
Successfully installed dacite-1.8.1 htmlmin-0.1.12 imagehash-4.3.1 multimethod-1.11.2 phik-0.12.4 typeguard-4.3.0 visions-0.7.6 ydata-profiling-4.8.3
In [2]:
# Cargo las bibliotecas necesarias
import numpy as np
import pandas as pd
import seaborn as sns
import random
import matplotlib.pyplot as plt
from ydata_profiling import ProfileReport
In [3]:
# Importo mi base de datos

df=pd.read_csv('/content/BD Bosque Secundario Florencia.csv')
print(df)
      Identificador de experimento        Nombre del experimento  \
0                                8  Bosque secundario Florencia    
1                                8  Bosque secundario Florencia    
2                                8  Bosque secundario Florencia    
3                                8  Bosque secundario Florencia    
4                                8  Bosque secundario Florencia    
...                            ...                           ...   
9072                             8  Bosque secundario Florencia    
9073                             8  Bosque secundario Florencia    
9074                             8  Bosque secundario Florencia    
9075                             8  Bosque secundario Florencia    
9076                             8  Bosque secundario Florencia    

     Nombre del bosque  Area_bosque_ha  Registro de la parcela  Parcela  \
0                CATIE              30                     118        1   
1                CATIE              30                     118        1   
2                CATIE              30                     118        1   
3                CATIE              30                     118        1   
4                CATIE              30                     118        1   
...                ...             ...                     ...      ...   
9072             CATIE              30                     129       12   
9073             CATIE              30                     129       12   
9074             CATIE              30                     129       12   
9075             CATIE              30                     129       12   
9076             CATIE              30                     129       12   

             Nombre de la parcela  Altitud de la parcela      y_WGS     x_WGS  \
0     Bosque secundario Florencia                    715 -83.671481  9.881741   
1     Bosque secundario Florencia                    715 -83.671481  9.881741   
2     Bosque secundario Florencia                    715 -83.671481  9.881741   
3     Bosque secundario Florencia                    715 -83.671481  9.881741   
4     Bosque secundario Florencia                    715 -83.671481  9.881741   
...                           ...                    ...        ...       ...   
9072  Bosque secundario Florencia                    598 -83.661325  9.874750   
9073  Bosque secundario Florencia                    598 -83.661325  9.874750   
9074  Bosque secundario Florencia                    598 -83.661325  9.874750   
9075  Bosque secundario Florencia                    598 -83.661325  9.874750   
9076  Bosque secundario Florencia                    598 -83.661325  9.874750   

      ...   Especie  Forma de vida Identificador  subparcela numero_arbol eje  \
0     ...  meiantha              7         46196           0            1   1   
1     ...  elastica              7         46197           0            2   1   
2     ...  lucidula              7         46198           0            3   1   
3     ...  pubivena              7         46199           0            4   1   
4     ...   pinnata              7         46200           0            5   1   
...   ...       ...            ...           ...         ...          ...  ..   
9072  ...  insignis              2         50343          44            7   1   
9073  ...  koschnyi              7         50344          44            8   1   
9074  ...  koschnyi              7         50345          44            9   1   
9075  ...  insignis              2         50346          44           10   1   
9076  ...  pubivena              7         50347          44           11   1   

     Identificador de fecha de medicion numero_medicion       Fecha  dap (mm)  
0                                   574               1  14/02/2018     570.0  
1                                   574               1  14/02/2018     157.0  
2                                   574               1  14/02/2018      57.0  
3                                   574               1  14/02/2018      90.0  
4                                   574               1  14/02/2018     103.0  
...                                 ...             ...         ...       ...  
9072                                678               3  23/11/2020      54.0  
9073                                678               3  23/11/2020      11.0  
9074                                678               3  23/11/2020      35.0  
9075                                678               3  23/11/2020      32.0  
9076                                678               3  23/11/2020      59.0  

[9077 rows x 27 columns]

Pasos previos¶

In [4]:
# Selecciono solo las columnas que me sean útiles

print("Antes:", df.columns, "\n")

df = df[['Parcela', 'Genero', 'Especie', 'Forma de vida', 'subparcela','numero_arbol', 'eje', 'numero_medicion', 'dap (mm)']]

print("Después:", df.columns)
print(df)
Antes: Index(['Identificador de experimento', 'Nombre del experimento',
       'Nombre del bosque', 'Area_bosque_ha', 'Registro de la parcela',
       'Parcela', 'Nombre de la parcela', 'Altitud de la parcela', 'y_WGS',
       'x_WGS', 'lat_CRTM05', 'long_CRTM05', 'Tratamiento', 'Area_parcela_ha',
       'Cod_sp', 'Familia', 'Genero', 'Especie', 'Forma de vida',
       'Identificador', 'subparcela', 'numero_arbol', 'eje',
       'Identificador de fecha de medicion', 'numero_medicion', 'Fecha',
       'dap (mm)'],
      dtype='object') 

Después: Index(['Parcela', 'Genero', 'Especie', 'Forma de vida', 'subparcela',
       'numero_arbol', 'eje', 'numero_medicion', 'dap (mm)'],
      dtype='object')
      Parcela      Genero   Especie  Forma de vida  subparcela  numero_arbol  \
0           1  Goethalsia  meiantha              7           0             1   
1           1    Castilla  elastica              7           0             2   
2           1      Cordia  lucidula              7           0             3   
3           1     Sorocea  pubivena              7           0             4   
4           1      Amyris   pinnata              7           0             5   
...       ...         ...       ...            ...         ...           ...   
9072       12      Ruagea  insignis              2          44             7   
9073       12      Virola  koschnyi              7          44             8   
9074       12      Virola  koschnyi              7          44             9   
9075       12      Ruagea  insignis              2          44            10   
9076       12     Sorocea  pubivena              7          44            11   

      eje  numero_medicion  dap (mm)  
0       1                1     570.0  
1       1                1     157.0  
2       1                1      57.0  
3       1                1      90.0  
4       1                1     103.0  
...   ...              ...       ...  
9072    1                3      54.0  
9073    1                3      11.0  
9074    1                3      35.0  
9075    1                3      32.0  
9076    1                3      59.0  

[9077 rows x 9 columns]

Modifico mi base de datos para crear nuevas columnas¶

Mi base de datos carece de una columna que sea un "identificador único" de cada individuo, por ello, crearé una.

In [5]:
# Modificar las columas

# Creo un identificador único por individuo

df['ID'] = 'IND' + '_' + df['Parcela'].astype(str) + '_' + df['subparcela'].astype(str) + '_' + df['numero_arbol'].astype(str)

# Creo un identificador único por eje

df['ID_eje'] = 'IND' + '_' + df['Parcela'].astype(str) + '_' + df['subparcela'].astype(str) + '_' + df['numero_arbol'].astype(str) + '_' + df['eje'].astype(str)

# Uno las columnas de género y especie para tener el nombre completo

df['nombre_cientifico'] = df['Genero'] + ' ' + df['Especie']

print(df)
      Parcela      Genero   Especie  Forma de vida  subparcela  numero_arbol  \
0           1  Goethalsia  meiantha              7           0             1   
1           1    Castilla  elastica              7           0             2   
2           1      Cordia  lucidula              7           0             3   
3           1     Sorocea  pubivena              7           0             4   
4           1      Amyris   pinnata              7           0             5   
...       ...         ...       ...            ...         ...           ...   
9072       12      Ruagea  insignis              2          44             7   
9073       12      Virola  koschnyi              7          44             8   
9074       12      Virola  koschnyi              7          44             9   
9075       12      Ruagea  insignis              2          44            10   
9076       12     Sorocea  pubivena              7          44            11   

      eje  numero_medicion  dap (mm)            ID          ID_eje  \
0       1                1     570.0     IND_1_0_1     IND_1_0_1_1   
1       1                1     157.0     IND_1_0_2     IND_1_0_2_1   
2       1                1      57.0     IND_1_0_3     IND_1_0_3_1   
3       1                1      90.0     IND_1_0_4     IND_1_0_4_1   
4       1                1     103.0     IND_1_0_5     IND_1_0_5_1   
...   ...              ...       ...           ...             ...   
9072    1                3      54.0   IND_12_44_7   IND_12_44_7_1   
9073    1                3      11.0   IND_12_44_8   IND_12_44_8_1   
9074    1                3      35.0   IND_12_44_9   IND_12_44_9_1   
9075    1                3      32.0  IND_12_44_10  IND_12_44_10_1   
9076    1                3      59.0  IND_12_44_11  IND_12_44_11_1   

        nombre_cientifico  
0     Goethalsia meiantha  
1       Castilla elastica  
2         Cordia lucidula  
3        Sorocea pubivena  
4          Amyris pinnata  
...                   ...  
9072      Ruagea insignis  
9073      Virola koschnyi  
9074      Virola koschnyi  
9075      Ruagea insignis  
9076     Sorocea pubivena  

[9077 rows x 12 columns]

Visualización de los registros del DAP¶

Con anticipación, se informó que está base de datos del bosque Florencia podía contener algunos registros "anormales" es la columna llamada dap (mm). Estos registros pueden ser igual a 0, lo que indica que el individuo estaba muerto al momento de la medición y también valores "NA" los cuales requieren de su eliminación o corrección según sea el caso. Para ello, cuantificaremos los registros anómalos para DAP con Pandas.

In [6]:
# Identifico los DAP con valores igual a cero en mi base de datos
valores_cero = df['dap (mm)'] == 0
conteo_cero = df.loc[valores_cero, ['ID_eje', 'numero_medicion']].value_counts()

print(conteo_cero)
ID_eje          numero_medicion
IND_10_10_3_1   2                  1
IND_2_31_8_2    2                  1
IND_6_24_7_1    2                  1
IND_6_24_3_1    2                  1
IND_6_23_5_1    1                  1
                                  ..
IND_11_43_11_1  3                  1
IND_11_43_10_1  3                  1
IND_11_42_3_1   2                  1
IND_11_42_1_1   2                  1
IND_9_4_9_1     2                  1
Name: count, Length: 417, dtype: int64
In [7]:
# Identifico los DAP con valores NA en mi base de datos
valores_nulos = df['dap (mm)'].isna()
conteo_nulos = df.loc[valores_nulos, ['ID_eje', 'numero_medicion']].value_counts()

print(conteo_nulos)
ID_eje         numero_medicion
IND_1_10_2_1   2                  1
IND_1_13_3_2   2                  1
IND_1_1_1_2    1                  1
IND_1_1_7_1    2                  1
IND_1_23_3_1   2                  1
IND_1_30_1_1   2                  1
IND_1_3_6_1    1                  1
IND_7_41_6_1   3                  1
IND_8_0_3_4    2                  1
IND_8_3_12_1   2                  1
IND_8_41_6_1   3                  1
IND_9_10_21_1  2                  1
IND_9_33_10_1  2                  1
Name: count, dtype: int64

Según la información mostrada gracias a Pandas, observamos que existen diversos registros con mediciones de DAP igual a cero. Así mismo, existen varias con valores NA en diversas mediciones a través del tiempo.

Corrijo datos faltantes de DAP¶

Para ello, identifico aquellas mediciones que posean NA en el dap. Dependiendo del número de medición, tomaré una decisión:

  1. Si el NA se encuentra en la primera medición, elimino el registro, ya que no es posible saber si ese árbol realmente existía en esa primera medición o apareció hasta la siguiente..

  2. Si el NA se encuentra en la segunda medición, y el árbol posee mediciones en la primera y tercera medición, se realizará un promedio.

  3. Si el NA se encuentra en la segunda medición y no tiene medición anterior o siguiente, se eliminará el registro ya que es posible que el árbol pueda haber muerto.

  4. Si el NA se encuentra en la tercera medición, se borrará el registro, ya que es probable de que haya muerto.

Es importante tomar en cuenta que estas reglas aplican para este conjunto de datos, los cuales poseen 3 mediciones. Si en el futuro se toman más mediciones, las reglas podrían cambiar.

In [8]:
# Elimino los registros con DAP igual a cero, ya que son indicativos de que el árbol murió

df = df[df['dap (mm)'] != 0]
In [9]:
# Crear una columna "reparado" para almacenar los resultados reparados
df['reparado'] = None

# Aplicamos las condicionales para establecer las reglas o decisiones que establecimos anteriormente
for index, row in df[valores_nulos].iterrows():
    ID_eje = row['ID_eje']
    num_medicion = row['numero_medicion']     # Especifico que quiero que tome en cuenta las columnas de ID_eje y numero_medicion

    if num_medicion in [1, 3]:
        df.at[index, 'reparado'] = False    # Primera regla
    elif num_medicion == 2:
        # Segunda regla: verificar si hay valores en numero_medicion 1 y 3 para el mismo ID_eje
        valor_1 = df[(df['ID_eje'] == ID_eje) & (df['numero_medicion'] == 1)]['dap (mm)'].values
        valor_3 = df[(df['ID_eje'] == ID_eje) & (df['numero_medicion'] == 3)]['dap (mm)'].values

        if len(valor_1) > 0 and not pd.isna(valor_1[0]) and len(valor_3) > 0 and not pd.isna(valor_3[0]):
            df.at[index, 'reparado'] = True
        else:
            df.at[index, 'reparado'] = False   # Tercera regla
<ipython-input-9-8b19e9b31009>:5: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for index, row in df[valores_nulos].iterrows():
In [10]:
# Verificamos la columna "reparado" en los ID que tenían NA en la columna dap (mm)
conteo_reparados = df.loc[valores_nulos, ['ID_eje', 'numero_medicion', 'reparado']].value_counts()

print(conteo_reparados)
ID_eje         numero_medicion  reparado
IND_1_10_2_1   2                True        1
IND_1_13_3_2   2                True        1
IND_1_1_1_2    1                False       1
IND_1_1_7_1    2                True        1
IND_1_23_3_1   2                False       1
IND_1_30_1_1   2                True        1
IND_1_3_6_1    1                False       1
IND_7_41_6_1   3                False       1
IND_8_0_3_4    2                False       1
IND_8_3_12_1   2                False       1
IND_8_41_6_1   3                False       1
IND_9_10_21_1  2                False       1
IND_9_33_10_1  2                False       1
Name: count, dtype: int64

Observaciones:¶

Según el resultado anterior, observamos que de todos los ID_eje que mostraron tener valores nulos en dap (mm), existen 4 de ellos que es posible aplicar una corrección. Esta corrección se realizará promediando la medición anterior con la siguiente.

Para todos los ID_eje que mostraron valores "False" se eliminarán, ya que no existe seguridad de si el árbol murió.

In [11]:
# Reparados los valores NA que hayan tenido "True" en la columna de reparado
for index, row in df[valores_nulos].iterrows():
    ID_eje = row['ID_eje']
    num_medicion = row['numero_medicion']

    if num_medicion in [1, 3]:
        df.at[index, 'reparado'] = False
    elif num_medicion == 2:
        # Verificar si hay valores en numero_medicion 1 y 3 para el mismo id_eje
        valor_1 = df[(df['ID_eje'] == ID_eje) & (df['numero_medicion'] == 1)]['dap (mm)'].values
        valor_3 = df[(df['ID_eje'] == ID_eje) & (df['numero_medicion'] == 3)]['dap (mm)'].values

        if len(valor_1) > 0 and not pd.isna(valor_1[0]) and len(valor_3) > 0 and not pd.isna(valor_3[0]):
            df.at[index, 'reparado'] = True
            # Calcular el promedio
            promedio = (valor_1[0] + valor_3[0]) / 2
            df.at[index, 'dap (mm)'] = promedio
        else:
            df.at[index, 'reparado'] = False

# Eliminamos los registros que obtuvieron "False" en la columna 'reparado'
df = df[df['reparado'] != False]
<ipython-input-11-c2cb66d34907>:2: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for index, row in df[valores_nulos].iterrows():
In [12]:
# Mostrar los registros corregidos
corregidos = df[df['reparado'] == True][['ID_eje', 'numero_medicion', 'dap (mm)']]
print("Registros corregidos:")
print(corregidos)
Registros corregidos:
            ID_eje  numero_medicion  dap (mm)
3061   IND_1_1_7_1                2      22.5
3098  IND_1_10_2_1                2     101.0
3120  IND_1_13_3_2                2     103.0
3192  IND_1_30_1_1                2     455.0
In [13]:
# Verifico si la corrección fue correctamente aplicada a uno de los individuos corregidos
filtro_individual = df[df['ID_eje'] == 'IND_1_1_7_1'][['numero_medicion', 'dap (mm)']]
print(filtro_individual)
      numero_medicion  dap (mm)
1445                1      22.0
3061                2      22.5
6307                3      23.0

Tomando como ejemplo el registro IND_1_1_7_1 podemos observar que la corrección fue ejecutada correctamente, ya que la medición 1 fue de 22 mm y la 3 de 23 mm, siendo el promedio de ambas mediciones de 22.5 mm

Suma de los diámetros de los ejes de cada individuo¶

Al medir un árbol, si éste posee más de un eje o "brazo" también debe ser medido. La suma de los ejes de un individuo es útil para calcular indicadores de estructura y funcionalidad, tales como área basal, biomasa y carbono. Por ello, se sumarán los ejes para cada individuo y eso me dará el dap final a partir del identificador único por individuo creado en el paso anterior.

In [14]:
# Crear la columna dap_total con ceros iniciales
df['dap_total'] = 0

# Agrupar por las columnas ID y número de medicion y sumar los ejes
grouped = df.groupby(['ID', 'numero_medicion'])['dap (mm)'].sum().reset_index()
grouped.rename(columns={'dap (mm)': 'dap_total'}, inplace=True)

# Merge con el df original
df = df.merge(grouped, on=['ID', 'numero_medicion'], how='left', suffixes=('', '_total'))
In [15]:
# Muestro el resultado de un individuo con más de un eje para comprobar que los resultados están bien
ind_multiples_ejes = df[df['ID'] == 'IND_3_22_1']
print(ind_multiples_ejes)
      Parcela  Genero    Especie  Forma de vida  subparcela  numero_arbol  \
231         3  Ocotea  rivularis              7          22             1   
232         3  Ocotea  rivularis              7          22             1   
233         3  Ocotea  rivularis              7          22             1   
234         3  Ocotea  rivularis              7          22             1   
3581        3  Ocotea  rivularis              7          22             1   
3582        3  Ocotea  rivularis              7          22             1   
3583        3  Ocotea  rivularis              7          22             1   
3584        3  Ocotea  rivularis              7          22             1   
6563        3  Ocotea  rivularis              7          22             1   
6564        3  Ocotea  rivularis              7          22             1   
6565        3  Ocotea  rivularis              7          22             1   
6566        3  Ocotea  rivularis              7          22             1   

      eje  numero_medicion  dap (mm)          ID        ID_eje  \
231     1                1     256.0  IND_3_22_1  IND_3_22_1_1   
232     2                1     126.0  IND_3_22_1  IND_3_22_1_2   
233     3                1     102.0  IND_3_22_1  IND_3_22_1_3   
234     4                1     101.0  IND_3_22_1  IND_3_22_1_4   
3581    1                2     257.0  IND_3_22_1  IND_3_22_1_1   
3582    2                2     150.0  IND_3_22_1  IND_3_22_1_2   
3583    3                2     133.0  IND_3_22_1  IND_3_22_1_3   
3584    4                2     120.0  IND_3_22_1  IND_3_22_1_4   
6563    1                3     263.0  IND_3_22_1  IND_3_22_1_1   
6564    2                3     164.0  IND_3_22_1  IND_3_22_1_2   
6565    3                3     151.0  IND_3_22_1  IND_3_22_1_3   
6566    4                3     131.0  IND_3_22_1  IND_3_22_1_4   

     nombre_cientifico reparado  dap_total  dap_total_total  
231   Ocotea rivularis     None          0            585.0  
232   Ocotea rivularis     None          0            585.0  
233   Ocotea rivularis     None          0            585.0  
234   Ocotea rivularis     None          0            585.0  
3581  Ocotea rivularis     None          0            660.0  
3582  Ocotea rivularis     None          0            660.0  
3583  Ocotea rivularis     None          0            660.0  
3584  Ocotea rivularis     None          0            660.0  
6563  Ocotea rivularis     None          0            709.0  
6564  Ocotea rivularis     None          0            709.0  
6565  Ocotea rivularis     None          0            709.0  
6566  Ocotea rivularis     None          0            709.0  

Observaciones:¶

Observamos que el ID: IND_3_22_1, de la especie Ocotea rivularis, posee 4 ejes, la suma de los 4 ejes da un total de 585 mm en la primera medición (182+134+143+136+= 585 mm); 660mm en la segunda medición y 709 mm en la tercera medición. Por lo que la columna dap_total_total muestra el resultado de la suma de los ejes de manera correcta. Lo que procede es eliminar los datos duplicados y columnas innecesarias.


Re ordeno y limpio mi base de datos¶

In [16]:
# Elimino columna de dap_total
df = df.drop(columns=['dap_total'])


# Elimino elementos duplicados basado en el ID, numero_medicion y dap_total_total
df = df.drop_duplicates(subset=['ID', 'numero_medicion', 'dap_total_total'])

# Le cambio el nombre a la columna dap_total_total
df = df.rename(columns={'dap_total_total': 'dap_cm'})

print(df)
      Parcela      Genero   Especie  Forma de vida  subparcela  numero_arbol  \
0           1  Goethalsia  meiantha              7           0             1   
1           1    Castilla  elastica              7           0             2   
2           1      Cordia  lucidula              7           0             3   
3           1     Sorocea  pubivena              7           0             4   
4           1      Amyris   pinnata              7           0             5   
...       ...         ...       ...            ...         ...           ...   
8646       12      Ruagea  insignis              2          44             7   
8647       12      Virola  koschnyi              7          44             8   
8648       12      Virola  koschnyi              7          44             9   
8649       12      Ruagea  insignis              2          44            10   
8650       12     Sorocea  pubivena              7          44            11   

      eje  numero_medicion  dap (mm)            ID          ID_eje  \
0       1                1     570.0     IND_1_0_1     IND_1_0_1_1   
1       1                1     157.0     IND_1_0_2     IND_1_0_2_1   
2       1                1      57.0     IND_1_0_3     IND_1_0_3_1   
3       1                1      90.0     IND_1_0_4     IND_1_0_4_1   
4       1                1     103.0     IND_1_0_5     IND_1_0_5_1   
...   ...              ...       ...           ...             ...   
8646    1                3      54.0   IND_12_44_7   IND_12_44_7_1   
8647    1                3      11.0   IND_12_44_8   IND_12_44_8_1   
8648    1                3      35.0   IND_12_44_9   IND_12_44_9_1   
8649    1                3      32.0  IND_12_44_10  IND_12_44_10_1   
8650    1                3      59.0  IND_12_44_11  IND_12_44_11_1   

        nombre_cientifico reparado  dap_cm  
0     Goethalsia meiantha     None   570.0  
1       Castilla elastica     None   157.0  
2         Cordia lucidula     None    57.0  
3        Sorocea pubivena     None    90.0  
4          Amyris pinnata     None   103.0  
...                   ...      ...     ...  
8646      Ruagea insignis     None    54.0  
8647      Virola koschnyi     None    11.0  
8648      Virola koschnyi     None    35.0  
8649      Ruagea insignis     None    32.0  
8650     Sorocea pubivena     None    59.0  

[8365 rows x 14 columns]
In [17]:
# Verifico nuevamente el ID con ejes múltiples para verificar que no hay duplicados

ind_multiples_ejescm = df[df['ID'] == 'IND_3_22_1']
print(ind_multiples_ejescm)
      Parcela  Genero    Especie  Forma de vida  subparcela  numero_arbol  \
231         3  Ocotea  rivularis              7          22             1   
3581        3  Ocotea  rivularis              7          22             1   
6563        3  Ocotea  rivularis              7          22             1   

      eje  numero_medicion  dap (mm)          ID        ID_eje  \
231     1                1     256.0  IND_3_22_1  IND_3_22_1_1   
3581    1                2     257.0  IND_3_22_1  IND_3_22_1_1   
6563    1                3     263.0  IND_3_22_1  IND_3_22_1_1   

     nombre_cientifico reparado  dap_cm  
231   Ocotea rivularis     None   585.0  
3581  Ocotea rivularis     None   660.0  
6563  Ocotea rivularis     None   709.0  

Convierto el dap de milímetros a centímetros¶

Para una mejor comprensión de las mediciones:

In [18]:
# Divido entre 10 para convertir los milímetros a centímetros
df['dap_cm'] = df['dap_cm'] / 10
print(df)
      Parcela      Genero   Especie  Forma de vida  subparcela  numero_arbol  \
0           1  Goethalsia  meiantha              7           0             1   
1           1    Castilla  elastica              7           0             2   
2           1      Cordia  lucidula              7           0             3   
3           1     Sorocea  pubivena              7           0             4   
4           1      Amyris   pinnata              7           0             5   
...       ...         ...       ...            ...         ...           ...   
8646       12      Ruagea  insignis              2          44             7   
8647       12      Virola  koschnyi              7          44             8   
8648       12      Virola  koschnyi              7          44             9   
8649       12      Ruagea  insignis              2          44            10   
8650       12     Sorocea  pubivena              7          44            11   

      eje  numero_medicion  dap (mm)            ID          ID_eje  \
0       1                1     570.0     IND_1_0_1     IND_1_0_1_1   
1       1                1     157.0     IND_1_0_2     IND_1_0_2_1   
2       1                1      57.0     IND_1_0_3     IND_1_0_3_1   
3       1                1      90.0     IND_1_0_4     IND_1_0_4_1   
4       1                1     103.0     IND_1_0_5     IND_1_0_5_1   
...   ...              ...       ...           ...             ...   
8646    1                3      54.0   IND_12_44_7   IND_12_44_7_1   
8647    1                3      11.0   IND_12_44_8   IND_12_44_8_1   
8648    1                3      35.0   IND_12_44_9   IND_12_44_9_1   
8649    1                3      32.0  IND_12_44_10  IND_12_44_10_1   
8650    1                3      59.0  IND_12_44_11  IND_12_44_11_1   

        nombre_cientifico reparado  dap_cm  
0     Goethalsia meiantha     None    57.0  
1       Castilla elastica     None    15.7  
2         Cordia lucidula     None     5.7  
3        Sorocea pubivena     None     9.0  
4          Amyris pinnata     None    10.3  
...                   ...      ...     ...  
8646      Ruagea insignis     None     5.4  
8647      Virola koschnyi     None     1.1  
8648      Virola koschnyi     None     3.5  
8649      Ruagea insignis     None     3.2  
8650     Sorocea pubivena     None     5.9  

[8365 rows x 14 columns]
In [19]:
# Verifico el registro reparado en cm
ind_multiples_ejescm = df[df['ID'] == 'IND_3_22_1']
print(ind_multiples_ejescm)
      Parcela  Genero    Especie  Forma de vida  subparcela  numero_arbol  \
231         3  Ocotea  rivularis              7          22             1   
3581        3  Ocotea  rivularis              7          22             1   
6563        3  Ocotea  rivularis              7          22             1   

      eje  numero_medicion  dap (mm)          ID        ID_eje  \
231     1                1     256.0  IND_3_22_1  IND_3_22_1_1   
3581    1                2     257.0  IND_3_22_1  IND_3_22_1_1   
6563    1                3     263.0  IND_3_22_1  IND_3_22_1_1   

     nombre_cientifico reparado  dap_cm  
231   Ocotea rivularis     None    58.5  
3581  Ocotea rivularis     None    66.0  
6563  Ocotea rivularis     None    70.9  

Limpiar la base de datos¶

En este caso, la columna "eje" ya no me es relevante, ya que ya tengo el dap total (dap_cm). Así mismo, no me es útil la columna "ID_eje", "dap (mm) y "reparado". Por lo que procedo a eliminarlas para simplificar mi base de datos

In [20]:
# Elimino columnas
df = df.drop(columns=['eje', 'ID_eje', 'dap (mm)', 'reparado'])

print('Columnas actuales:', df.columns)
Columnas actuales: Index(['Parcela', 'Genero', 'Especie', 'Forma de vida', 'subparcela',
       'numero_arbol', 'numero_medicion', 'ID', 'nombre_cientifico', 'dap_cm'],
      dtype='object')

Creación de Pandas-profiling¶

Con la creación de este informe podemos ver de manera gráfica otros atributos de la base de datos junto con sus estadísticas.

In [21]:
# Creamos el informe con pandas-profiling
nombre = "Bosque Florencia"
profile = ProfileReport(df, title=nombre, explorative=True)

# Mostrar el informe en un notebook
profile.to_notebook_iframe()
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]

Indicadores y gráficos¶

A continuación se mostrarán una serie de gráficos para observar la distribución o comportamiento de nuestros datos respecto a los indicadores de biodiversidad y estructura calculados

Indicadores de biodiversidad¶

1. Formas de vida¶

In [22]:
# Calculamos la frecuencia de las distitns formas de vida en mi df
frecuencia = df['Forma de vida'].value_counts()

# Calculamos los porcentajes para mejor entendimiento
porcentajes = (frecuencia / frecuencia.sum()) * 100

# Gráfico de barras
plt.figure(figsize=(10, 6))
sns.barplot(x=porcentajes.index, y=porcentajes.values, palette='viridis')
plt.title('Frecuencia de las Formas de Vida')
plt.xlabel('Forma de Vida')
plt.ylabel('Porcentaje (%)')
plt.show()
<ipython-input-22-4671fbe296f3>:9: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.barplot(x=porcentajes.index, y=porcentajes.values, palette='viridis')
No description has been provided for this image

Observamos que cerca del 90 % de los individios pertenecen a la forma de vida con código 7, los cuales son árboles. Cerca del 10 % a la forma de vida 2, los cuales también son árboles y un ínfimo porcentaje son palmas, herbáceas, musáceas e individuos sin identificación taxonómica conocida.

2. Riqueza¶

In [23]:
# Riqueza de especies
# Calcular la riqueza de especies por parcela y numero_medicion
riqueza_especies = df.groupby(['Parcela', 'numero_medicion'])['nombre_cientifico'].nunique().reset_index()
riqueza_especies.columns = ['Parcela', 'numero_medicion', 'riqueza_especies']

print(riqueza_especies)
    Parcela  numero_medicion  riqueza_especies
0         1                1                37
1         1                2                36
2         1                3                37
3         2                1                29
4         2                2                30
5         2                3                30
6         3                1                44
7         3                2                43
8         3                3                42
9         4                1                42
10        4                2                40
11        4                3                39
12        5                1                54
13        5                2                54
14        5                3                53
15        6                1                47
16        6                2                46
17        6                3                46
18        7                1                50
19        7                2                52
20        7                3                55
21        8                1                40
22        8                2                39
23        8                3                40
24        9                1                57
25        9                2                57
26        9                3                59
27       10                1                50
28       10                2                42
29       10                3                47
30       11                1                46
31       11                2                46
32       11                3                47
33       12                1                49
34       12                2                48
35       12                3                49
In [24]:
# Riqueza de especies
# Creo diferentes estilos de líneas para diferenciar las parcelas
linestyles = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.', ':']
markers = ['o', 's', 'D', '^', 'v', '<', '>', 'p', 'h', '8', '*', 'X']

# Defino los colores
palette = sns.color_palette("husl", len(riqueza_especies['Parcela'].unique()))

# Creo el gráfico
plt.figure(figsize=(12, 8))
for i, (parcela, group) in enumerate(riqueza_especies.groupby('Parcela')):
    sns.lineplot(data=group, x='numero_medicion', y='riqueza_especies', marker=markers[i], linestyle=linestyles[i],
                 label=parcela, color=palette[i])

plt.title('Riqueza de especies por parcela a través del tiempo')
plt.xlabel('Número de medición')
plt.ylabel('Riqueza de especies')
plt.legend(title='Parcela')
plt.grid(True)
No description has been provided for this image

Como vemos en el gráfico de riqueza, la parcela 9 es la que mayor riqueza de especies posee y la parcela 2 la que menos tiene. Así mismo,vemos que en la mayoría de parcelas, la dinámica de la riqueza parece ir en aumento.

3. Índice de diversidad de Shannon¶

In [25]:
# Índice de Diversidad de Shannon

# Calcular el índice de Shannon para cada parcela y numero_medicion
def shannon_index(group):
    proportions = group.value_counts(normalize=True)
    return -sum(proportions * np.log(proportions))

shannon_diversity = df.groupby(['Parcela', 'numero_medicion'])['nombre_cientifico'].apply(shannon_index).reset_index()

shannon_diversity.columns = ['Parcela', 'numero_medicion', 'shannon_index']

print(shannon_diversity)
    Parcela  numero_medicion  shannon_index
0         1                1       2.661694
1         1                2       2.672911
2         1                3       2.695538
3         2                1       2.341219
4         2                2       2.367814
5         2                3       2.425674
6         3                1       3.231613
7         3                2       3.228367
8         3                3       3.198772
9         4                1       3.348448
10        4                2       3.297483
11        4                3       3.265917
12        5                1       3.523031
13        5                2       3.533413
14        5                3       3.514452
15        6                1       3.192095
16        6                2       3.159595
17        6                3       3.140759
18        7                1       3.157759
19        7                2       3.223356
20        7                3       3.257773
21        8                1       2.919093
22        8                2       2.904007
23        8                3       2.888724
24        9                1       3.065676
25        9                2       3.085156
26        9                3       3.096669
27       10                1       2.803773
28       10                2       2.853916
29       10                3       2.867757
30       11                1       3.234971
31       11                2       3.165768
32       11                3       3.121807
33       12                1       3.434803
34       12                2       3.382448
35       12                3       3.363404
In [26]:
# Escojo la paleta de colores y diferenciación de líneas
plt.figure(figsize=(12, 8))
for i, (parcela, group) in enumerate(shannon_diversity.groupby('Parcela')):
    sns.lineplot(data=group, x='numero_medicion', y='shannon_index', marker=markers[i], linestyle=linestyles[i],
                 label=parcela, color=palette[i])

# Creo el gráfico
plt.title('Índice de Diversidad de Shannon')
plt.xlabel('Número de medición')
plt.ylabel('Índice de Diversidad de Shannon')
plt.legend(title='Parcela')
plt.grid(True)
No description has been provided for this image

La riqueza de especies es el número de distintas especies que existen en un área estudiada. Por otro lado, el índice de diversidad de Shannon no solo toma en cuenta la riqueza sino la equitatividad con la que se distribuyen. Como observamos en la gráfica, la parcela 5 es la que posee el índice más alto, lo que indica que las especies están más equitativamente distribuidas en esta parcela. El índice de Shannon hace la pregunta: ¿qué probabilidad hay de que si selecciono por segunda vez a un individuo, sea de la misma especie que el individuo que seleccioné primero?

Indicadores de estructura¶

A continuación, mediremos los indicadores de estructura, los cuales incluyen a la caracterización de número de individuos, diámetros y de área basal por parcela.

In [27]:
# Número de individuos
# Agrupa por parcela y toma el número máximo
max_num_indiviudos = df.groupby('Parcela')['numero_arbol'].max().reset_index()

# Crear el gráfico de barras
plt.figure(figsize=(10, 6))
sns.barplot(x='Parcela', y='numero_arbol', data=max_num_indiviudos, palette='viridis')
plt.title('Número de individuos por parcela')
plt.xlabel('Parcela')
plt.ylabel('No. Individuos')
plt.show()
<ipython-input-27-2e520ba35725>:7: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.barplot(x='Parcela', y='numero_arbol', data=max_num_indiviudos, palette='viridis')
No description has been provided for this image
In [28]:
# Vemos los datos del gráfico
max_num_individuos = df.groupby('Parcela')['numero_arbol'].max().reset_index()
print(max_num_individuos)
    Parcela  numero_arbol
0         1            20
1         2            22
2         3            16
3         4            19
4         5            24
5         6            25
6         7            28
7         8            22
8         9            23
9        10            29
10       11            22
11       12            17

Tanto en el gráfico como en los datos, observamos que las parcelas con mayor cantidad de individuos son las parcelas 7 (28 individuos) y parcela 10 (29 individuos). Mientras que la parcela que posee la menor cantidad es la parcela 3 (16 individuos).

In [29]:
# Diámetros en las parcelas
# Calcular el diámetro mínimo, promedio y máximo para cada parcela
diametros = df.groupby('Parcela')['dap_cm'].agg(['min', 'mean', 'max']).reset_index()

# Renombrar las columnas para mayor claridad
diametros.columns = ['Parcela', 'Diámetro mínimo (cm)', 'Diámetro promedio (cm)', 'Diámetro máximo (cm)']

# Mostrar el DataFrame resultante
print(diametros)
    Parcela  Diámetro mínimo (cm)  Diámetro promedio (cm)  \
0         1                   0.7               13.746068   
1         2                   0.5               10.936883   
2         3                   0.6               13.685937   
3         4                   0.1               13.646417   
4         5                   0.5               14.135704   
5         6                   0.1               10.219034   
6         7                   0.6               12.258529   
7         8                   0.7               13.732081   
8         9                   0.5                9.868354   
9        10                   0.6               10.224724   
10       11                   0.6               10.203091   
11       12                   0.5               13.333435   

    Diámetro máximo (cm)  
0                   91.7  
1                   80.0  
2                   79.0  
3                   75.8  
4                   81.7  
5                   60.4  
6                   73.3  
7                  106.1  
8                   61.0  
9                   80.1  
10                  91.0  
11                  94.5  
In [30]:
# Encontrar el máximo, mínimo y promedio
# Calcular el valor mínimo del diámetro mínimo, el valor máximo del diámetro máximo y el valor promedio del diámetro promedio
valor_min_diametro_min = diametros['Diámetro mínimo (cm)'].min()
valor_max_diametro_max = diametros['Diámetro máximo (cm)'].max()
valor_prom_diametro_prom = diametros['Diámetro promedio (cm)'].mean()

print(f'Valor mínimo del diámetro mínimo (cm): {valor_min_diametro_min}')
print(f'Valor máximo del diámetro máximo (cm): {valor_max_diametro_max}')
print(f'Valor promedio del diámetro promedio (cm): {valor_prom_diametro_prom:.2f}')
Valor mínimo del diámetro mínimo (cm): 0.1
Valor máximo del diámetro máximo (cm): 106.1
Valor promedio del diámetro promedio (cm): 12.17
In [31]:
# Área basal
# Calcular el área basal para cada árbol
df['area_basal'] = np.pi * (df['dap_cm'] / 200) ** 2

# Calcular el área basal total por parcela y número de medición
area_basal_total = df.groupby(['Parcela', 'numero_medicion'])['area_basal'].sum().reset_index()
area_basal_total.columns = ['Parcela', 'numero_medicion', 'area_basal_total']

print(area_basal_total)
    Parcela  numero_medicion  area_basal_total
0         1                1          6.874764
1         1                2          7.241561
2         1                3          7.337370
3         2                1          6.070906
4         2                2          6.459865
5         2                3          6.622305
6         3                1          5.678742
7         3                2          5.281251
8         3                3          4.955061
9         4                1          5.348217
10        4                2          5.003114
11        4                3          4.732304
12        5                1          6.088758
13        5                2          6.180558
14        5                3          6.197104
15        6                1          4.107674
16        6                2          4.442741
17        6                3          4.627872
18        7                1          6.300415
19        7                2          5.215439
20        7                3          4.490798
21        8                1          7.190916
22        8                2          7.197110
23        8                3          7.485630
24        9                1          5.437246
25        9                2          5.034596
26        9                3          4.480501
27       10                1          5.389372
28       10                2          2.879881
29       10                3          5.258837
30       11                1          6.065030
31       11                2          4.338992
32       11                3          4.048172
33       12                1          6.470091
34       12                2          4.932245
35       12                3          5.236323
In [32]:
# Escojo una paleta de colores
palette = sns.color_palette("tab20", 12)

# Creo distintos estilos para diferenciar las líneas
linestyles = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.', ':']
markers = ['o', 's', 'D', '^', 'v', '<', '>', 'p', 'h', '8', '*', 'X']

# Crear un gráfico de líneas con la paleta de colores personalizada y estilos
plt.figure(figsize=(12, 8))
for i, (parcela, group) in enumerate(area_basal_total.groupby('Parcela')):
    sns.lineplot(data=group, x='numero_medicion', y='area_basal_total', marker=markers[i], linestyle=linestyles[i],
                 label=parcela, color=palette[i])

# Configuración del gráfico
plt.title('Área basal por parcela')
plt.xlabel('Número de medición')
plt.ylabel('Área basal (m²)')
plt.legend(title='Parcela')
plt.grid(True)
No description has been provided for this image

El área basal (G) puede ser calculada a nivel de parcela o a nivel del individuo. A nivel del invidiuo, es el área de la sección transversal del tronco del árbol a la altura del pecho. A nivel de parcela, es la suma de todas las áreas basales de todos los individuos por hectárea (Mercker, 2023). El área basal es una variable utilizada para estimar el volumen de madera, la biomasa arriba del suelo y carbono almacenado en bosques (Chiba, 1998; Torres & Lovett, 2013).

Observamos según la gráfica que la mayor área basal está en la parcela 8, en la cual también muestra un incremento a lo largo de los años. Mientras que la parcela 10 fue la que tuvo un drececimiento abrupto en la segunda medición y luego una recuperación en la tercera.

Por otro lado, se observa que no necesariamente todas las áreas basales están en aumento, sino que algunas parcelas están disminuyendo, como en la 3, 7, 9 y 11.

Descripción de los resultados obtenidos:¶

El bosque Florencia es un bosque secundario tropical ubicado en Turrialba, Costa Rica. Es un bosque relativamente "nuevo" y a pesar de ello, cuenta con tres mediciones a través del tiempo, las cuales evidencian distintas dinámicas entre las 12 parcelas medidas por el CATIE. A continuación se presentan los principales resultados obtenidos a través de las mediciones de indicadores estructurales y de biodiversidad.


1. Indicadores estructurales¶

  • Individuos por parcela

La parcela que posee mayor cantidad de individuos es la 9, mientras que la que menos posee es la número 3. Por lo que la parcela 9 no solo posee la mayor cantidad de especies presentes sino también la mayor cantidad de individuos muestreados. Sin embargo, la parcela 3 no mostró anteriormente índices bajos en sus indicadores de biodiversidad, por lo que pueda no tener la mayor cantidad de individuos muestreados pero en términos de cantidad de especies, se encuentra relativamente bien.

Además, con profiling también fue posible observar que para la última medición, fue mayor la cantidad de individuos muestreados respecto a la segunda y primera. Con ello, se evidencia que la complejidad y biodiversidad del bosque aumentan conforme pasa el tiempo, tal y como lo relatan diversos autores (Chokkalingam, et al. 2001; Poorter et al., 2021; Chazdon et al., 2009).

  • Diámetro a la altura del pecho de los individuos

Antes de observar las dinámicas de las parcelas a través del tiempo, fue necesario realizar correcciones para el DAP, ya que los valores nulos e igual a cero no eran posibles de procesar. Para ello, se corrigieron aquellos datos que poseían mediciones antes y después para realizar un promedio. Las mediciones donde no fue posible ejecutar esta acción fueron eliminados.

Únicamente se lograron corregir 4 DAP de los valores nulos, mientras que 9 restantes fueron eliminados junto con los 417 registros con valores igual a cero.

Los individuos dentro de las parcelas poseían mediciones de DAP por cada eje. Para cálculos posteriores como el área basal, es necesario realizar una sumatoria de ejes por individuo. Por ello, se creó un identificador único para cada individuo el cual fue útil para determinar si tenía uno o varios ejes en cada medición. De poseer más de un eje se procedió a realizar una sumatoria para obtener el DAP total en centímetros.

Gracias a Panda-profiling y la utilización de Pandas, se evidenció que el diámetro mínimo registrado en el bosque Florencia fue de 0.1 cm y el más grande de 106.1 cm, siendo el promedio general de 12.03 cm. La parcela 5 la que posee un diámetro promedio más alto y la parcela 9 la que posee el diámetro promedio más bajo. Por otro lado, el árbol con mayor diámetro en el bosque Florencia estudiado se encuentra en la parcela 7.

El histograma muestra que muy pocos individuos llegan al diámetro mínimo de corta para el país de 60 cm (MINAE, 1996) por lo que categoriza al bosque Florencia en un bosque en recuperación, aún no apto para un aprovechamiento.

  • Área basal

Para el área basal, existen valores desde 2m2 hasta poco más de 7m2 por parcela. Existen fluctuaciones para ciertas parcelas a través del tiempo, como el caso de la parcela 10, en donde en la segunda medición tuvo una recaida y luego se recuperó al año siguiente, lo cual no es extraño en bosques altamente dinámicos.

La parcela que posee una mayor área basal es la 8, la cual pueda tener una distribución más equitativa de los diámetros promedio o arriba del promedio. Además, otro aspecto importante en la dinámica del área basal es que no todas las parcelas parecen tener una tendencia fija a aumentar su área basal. Sin embargo, por el dinamismo de los bosques tropicales, es necesario obtener más mediciones a través del tiempo para observa una tendencia más clara.


2. Indicadores de biodiversidad¶

  • Formas de vida

A lo largo de toda la base de datos, observamos que la forma de vida más abundante fueron los árboles, en una proporción casi insigificante encontramos palmas, musáceas, herbáceas e individuos sin identificación taxonómica. Al estar estudiando un bosque y no un pastizal, sabana o párramo, es normal encontrar que las especies dominantes sean árboles.

  • Dominancia de especies

A través de profiling fue posible observar otras características de la base de datos. Por ejemplo, el género más frecuente fue Virola (26.6 %) seguido por Guarea (9.5 %) lo que indica una alta dominancia del género Virola respecto al resto de géneros.

  • Riqueza e Índice de Shannon

De los indicadores de biodiversidad, el cálculo de riqueza reflejó que la tendencia en la mayoría de las parcelas ha sido el aumento de la riqueza con el tiempo. Siendo la parcela 9 la que posee mayor riqueza. Sin embargo, observando los resultados del índice de diversidad de Shannon, observamos que la parcela 5 es la que posee un índice más alto y la 9 posee un índice medio. Por lo que a pesar de que existen mayor número de especies distintas en la parcela 9, las abundancias de las especies se encuentran mejor distribuidas (más equitativamente) en la parcela 5. Observamos que pueda que al ver los datos numéricos sea un tanto complicado de diferenciar cuál es la parcela con mayores indicadores, pero al realizar las gráficas se evidencia mejor la información por parcela y a través del tiempo.

Conclusiones:¶

  1. El bosque Florencia es un bosque secundario en recuperación y eso se evidencia con las tendencias de crecimiento en el área basal, riqueza y bioridiversidad, sin embargo, los bosques tropicales son altamente dinámicos por lo que los incrementos de estos índices no siempre son lineales.
  2. Los indicadores de biodiversidad y de estructura no necesarimente son directamente proporcionales. Es posible obtener mayores áreas basales con un mayor número de individuos o mayores diámetros sin necesariamente tener mayor cantidad y equitatividad de especies y viceversa.
  3. La utilización de Pandas-profiling facilita la visualización de datos, sobre todo aquellos en los que las medidas estadísticas clásicas son útiles o bien, aquellos datos en formato de texto, ya que profiling mejora su visualización, como fue en el caso de los nombres científicos más comunes en nuestra base de datos.
  4. Pandas comprende una herramienta sumamente útil para la visualización, transformación y presentación de datos. Gracias a ello fue posible visualizar datos faltantes, corregir, depurar la base de datos, calcular métricas y presentar los resultados no solo en formato numérico sino también con gráficos, lo que facilitaba su comprensión.

Bibliografía:¶

  1. Acharya, K. P., Dangi, R. B., & Acharya, M. (2011). Understanding forest degradation in Nepal. Unasylva, 62(238).
  2. Brandon, K. (2015). Ecosystem Services from Tropical Forests: Review of Current Science. SSRN Electronic Journal. https://doi.org/10.2139/ssrn.2622749
  3. Camacho-Calvo, M., Delgado-Rodríguez, D., Valera Mejías, V., & Serrano-Molina, J. J. (2021). Potencial productivo de cuatro bosques secundarios en América Central y pautas para su manejo silvícola. Centro Agronómico Tropical de Investigación y Enseñanza (CATIE).
  4. Chazdon, R. L., Peres, C. A., Dent, D., Sheil, D., Lugo, A. E., Lamb, D., Stork, N. E., & Miller, S. E. (2009). The potential for species conservation in tropical secondary forests. Conservation Biology, 23(6). https://doi.org/10.1111/j.1523-1739.2009.01338.x
  5. Chiba, Y. (1998). Architectural analysis of relationship between biomass and basal area based on pipe model theory. Ecological Modelling, 108(1–3). https://doi.org/10.1016/S0304-3800(98)00030-1
  6. Chokkalingam, U., & De Jong, W. (2001). Secondary forest: A working definition and typology. International Forestry Review, 3(1).
  7. Eguiguren, P., Fischer, R., & Günter, S. (2019). Degradation of ecosystem services and deforestation in landscapes with and without incentive-based forest conservation in the Ecuadorian Amazon. Forests, 10(5). https://doi.org/10.3390/f10050442
  8. FAO. (2020). Global Forest Resources Assessment 2020. In Global Forest Resources Assessment 2020. https://doi.org/10.4060/ca8753en
  9. Ganivet, E., & Bloomberg, M. (2019). Towards rapid assessments of tree species diversity and structure in fragmented tropical forests: A review of perspectives offered by remotely-sensed and field-based data. In Forest Ecology and Management (Vol. 432). https://doi.org/10.1016/j.foreco.2018.09.003
  10. Gerwing, J. J. (2002). Degradation of forests through logging and fire in the eastern Brazilian Amazon. Forest Ecology and Management, 157(1–3). https://doi.org/10.1016/S0378-1127(00)00644-7
  11. Longo, M., Keller, M., dos-Santos, M. N., Leitold, V., Pinagé, E. R., Baccini, A., Saatchi, S., Nogueira, E. M., Batistella, M., & Morton, D. C. (2016). Aboveground biomass variability across intact and degraded forests in the Brazilian Amazon. Global Biogeochemical Cycles, 30(11). https://doi.org/10.1002/2016GB005465
  12. Malhi, Y., Gardner, T. A., Goldsmith, G. R., Silman, M. R., & Zelazowski, P. (2014). Tropical forests in the anthropocene. In Annual Review of Environment and Resources (Vol. 39). https://doi.org/10.1146/annurev-environ-030713-155141
  13. Mercker, D. (2023). A simple guide to common forest measurements.
  14. MINAE, C. R. (1996). Ley Forestal No 7575: Los Principios, Criterios e Indicadores para el Manejo de Bosques Naturales y su Certificación en Costa Rica.
  15. Misiukas, J. M., Carter, S., & Herold, M. (2021). Tropical forest monitoring: Challenges and recent progress in research. Remote Sensing, 13(12). https://doi.org/10.3390/rs13122252
  16. Mullan, K. (2015). The Value of Forest Ecosystem Services to Developing Economies. SSRN Electronic Journal. https://doi.org/10.2139/ssrn.2622748
  17. Poorter, L., Rozendaal, D. M. A., Bongers, F., de Jarcilene, S. A., Àlvarez, F. S., Luìs Andrade, J., Arreola Villa, L. F., Becknell, J. M., Bhaskar, R., Boukili, V., Brancalion, P. H. S., Cèsar, R. G., Chave, J., Chazdon, R. L., Colletta, G. D., Craven, D., de Jong, B. H. J., Denslow, J. S., Dent, D. H., … Westoby, M. (2021). Functional recovery of secondary tropical forests. Proceedings of the National Academy of Sciences of the United States of America, 118(49). https://doi.org/10.1073/pnas.2003405118
  18. Putz, F. E., & Romero, C. (2014). Futures of tropical forests (sensu lato). Biotropica, 46(4). https://doi.org/10.1111/btp.12124
  19. Saatchi, S. S., Harris, N. L., Brown, S., Lefsky, M., Mitchard, E. T. A., Salas, W., Zutta, B. R., Buermann, W., Lewis, S. L., Hagen, S., Petrova, S., White, L., Silman, M., & Morel, A. (2011). Benchmark map of forest carbon stocks in tropical regions across three continents. Proceedings of the National Academy of Sciences of the United States of America, 108(24). https://doi.org/10.1073/pnas.1019576108
  20. Torres, A. B., & Lovett, J. C. (2013). Using basal area to estimate aboveground carbon stocks in forests: La Primavera Biosphere’s Reserve, Mexico. Forestry, 86(2). https://doi.org/10.1093/forestry/cps084
  21. Turner, I. M., Wong, Y. K., Chew, P. T., & Bin Ibrahim, A. (1997). Tree species richness in primary and old secondary tropical forest in Singapore. Biodiversity and Conservation, 6(4). https://doi.org/10.1023/A:1018381111842
  22. Wright, S. J. (2010). The future of tropical forests. In Annals of the New York Academy of Sciences (Vol. 1195). https://doi.org/10.1111/j.1749-6632.2010.05455.x