TP 2 - ANALYSE BIVARIEE¶
1) Analyse bivariée entre deux variables quantitatives¶
Import des librairies Python :
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import geopandas as gpd
Ouverture du jeu de données filosofi :
filosofi = gpd.read_file("carreaux_1km_met.shp")
filosofi.head()#Inspecter les 5 premières lignes du jeu de données
| idcar_1km | i_est_1km | lcog_geo | ind | men | men_pauv | men_1ind | men_5ind | men_prop | men_fmp | ... | ind_6_10 | ind_11_17 | ind_18_24 | ind_25_39 | ind_40_54 | ind_55_64 | ind_65_79 | ind_80p | ind_inc | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | CRS3035RES1000mN2029000E4252000 | 1 | 2A041 | 3.0 | 1.4 | 0.3 | 0.5 | 0.1 | 0.8 | 0.1 | ... | 0.2 | 0.2 | 0.3 | 0.2 | 0.6 | 0.2 | 1.0 | 0.3 | 0.0 | POLYGON ((1218038.251 6048790.697, 1217949.529... |
| 1 | CRS3035RES1000mN2029000E4254000 | 1 | 2A041 | 2.0 | 0.9 | 0.2 | 0.3 | 0.0 | 0.5 | 0.0 | ... | 0.1 | 0.1 | 0.2 | 0.1 | 0.4 | 0.1 | 0.7 | 0.3 | 0.0 | POLYGON ((1220028.075 6048968.632, 1219939.34 ... |
| 2 | CRS3035RES1000mN2030000E4252000 | 1 | 2A041 | 6.0 | 2.5 | 0.3 | 0.3 | 0.1 | 1.6 | 0.5 | ... | 0.1 | 0.5 | 0.3 | 0.7 | 1.5 | 1.4 | 1.1 | 0.3 | 0.0 | POLYGON ((1217949.529 6049793.871, 1217860.808... |
| 3 | CRS3035RES1000mN2030000E4253000 | 1 | 2A041 | 16.5 | 6.9 | 0.7 | 1.0 | 0.3 | 4.3 | 1.3 | ... | 0.3 | 1.3 | 1.0 | 2.0 | 4.0 | 3.7 | 3.0 | 0.7 | 0.0 | POLYGON ((1218944.435 6049882.842, 1218855.707... |
| 4 | CRS3035RES1000mN2030000E4254000 | 1 | 2A041 | 5.0 | 2.4 | 0.5 | 0.9 | 0.1 | 1.4 | 0.1 | ... | 0.2 | 0.4 | 0.5 | 0.4 | 1.0 | 0.4 | 1.6 | 0.5 | 0.0 | POLYGON ((1219939.34 6049971.805, 1219850.606 ... |
5 rows × 32 columns
On va étudier le lien entre les deux variables choisies suivantes :
- ind_80p (Population de plus de 80 ans)
- log_av45 (Nombre de ménages dans un logement d'avant 1945)
!pip install mapclassify
Collecting mapclassify Downloading mapclassify-2.10.0-py3-none-any.whl.metadata (3.1 kB) Requirement already satisfied: networkx>=3.2 in /usr/local/lib/python3.12/dist-packages (from mapclassify) (3.5) Requirement already satisfied: numpy>=1.26 in /usr/local/lib/python3.12/dist-packages (from mapclassify) (2.0.2) Requirement already satisfied: pandas>=2.1 in /usr/local/lib/python3.12/dist-packages (from mapclassify) (2.2.2) Requirement already satisfied: scikit-learn>=1.4 in /usr/local/lib/python3.12/dist-packages (from mapclassify) (1.6.1) Requirement already satisfied: scipy>=1.12 in /usr/local/lib/python3.12/dist-packages (from mapclassify) (1.16.3) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas>=2.1->mapclassify) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas>=2.1->mapclassify) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas>=2.1->mapclassify) (2025.2) Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn>=1.4->mapclassify) (1.5.2) Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn>=1.4->mapclassify) (3.6.0) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas>=2.1->mapclassify) (1.17.0) Downloading mapclassify-2.10.0-py3-none-any.whl (882 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 882.2/882.2 kB 13.8 MB/s eta 0:00:00 Installing collected packages: mapclassify Successfully installed mapclassify-2.10.0
import mapclassify
var_1 = "ind_80p"
var_2 = "log_av45"
filosofi.plot(figsize=(10,10), column=var_1, legend=True, scheme="quantiles", k=5).set_title(var_1)
filosofi.plot(figsize=(10,10), column=var_2, legend=True, scheme="quantiles", k=5).set_title(var_2)
filosofi.plot(figsize=(10,10), kind="scatter", x=var_1, y=var_2)
<Axes: xlabel='ind_80p', ylabel='log_av45'>
On voit que les distributions des deux variables sont très asymétriques vers la droite. On va donc refaire le scatter plot avec une échelle logarithmique. On peut aussi se questionner sur la présence de valeurs non entières sur les cartes. Elles viennent en fait des valeurs "imputées".
ax = filosofi.plot(figsize=(10,10), kind="scatter", x=var_1, y=var_2, c=np.array(["blue", "green", "red"])[filosofi["i_est_1km"]], s=0.1)
ax.set_xscale("log")
ax.set_yscale("log")
1.2) Corrélation¶
Un lien statistique semble bien exister entre les deux variables. On va donc calculer la corrélation entre les deux :
from scipy.stats import spearmanr, pearsonr
print("Corrélation de Pearson : ", pearsonr(filosofi[var_1], filosofi[var_2]))
print("Corrélation de Spearman : ", spearmanr(filosofi[var_1], filosofi[var_2]))
Corrélation de Pearson : PearsonRResult(statistic=np.float64(0.7322642873072113), pvalue=np.float64(0.0)) Corrélation de Spearman : SignificanceResult(statistic=np.float64(0.7405625506170361), pvalue=np.float64(0.0))
La corrélation est assez forte (R et $\rho$ >0.7), positive et significative (p-value très faible).
Cependant, ces deux variables sont en fait liées toutes les deux au nombre total d'habitants! On va donc diviser chacune par ind.
print("Corrélation de Pearson entre var_1 et ind : ", pearsonr(filosofi[var_1], filosofi["ind"]))
print("Corrélation de Pearson entre var_2 et ind : ", pearsonr(filosofi[var_2], filosofi["ind"]))
Corrélation de Pearson entre var_1 et ind : PearsonRResult(statistic=np.float64(0.9127798594694922), pvalue=np.float64(0.0)) Corrélation de Pearson entre var_2 et ind : PearsonRResult(statistic=np.float64(0.774901356046622), pvalue=np.float64(0.0))
filosofi["proportion_habitant_sup_80"] = filosofi["ind_80p"] / filosofi["ind"]
filosofi["proportion_habitant_dans_avant_1945"] = filosofi["log_av45"] / filosofi["ind"]
filosofi.plot(figsize=(10,10), column="proportion_habitant_sup_80", legend=True, scheme="quantiles", k=5).set_title(var_1+" par habitant")
filosofi.plot(figsize=(10,10), column="proportion_habitant_dans_avant_1945", legend=True, scheme="quantiles", k=5).set_title(var_2+" par habitant")
filosofi.plot(figsize=(10,10), kind="scatter", x="proportion_habitant_sup_80", y="proportion_habitant_dans_avant_1945")
<Axes: xlabel='proportion_habitant_sup_80', ylabel='proportion_habitant_dans_avant_1945'>
Les données sont écrasées vers les faibles valeurs. On va afficher le scatter plot avec une échelle logarithmique pour mieux étaler les valeurs.
ax = filosofi.plot(figsize=(10,10), kind="scatter", x="proportion_habitant_sup_80", y="proportion_habitant_dans_avant_1945")
ax.set_xscale("log")
ax.set_yscale("log")
print("Corrélation de Pearson : ", pearsonr(filosofi["proportion_habitant_sup_80"], filosofi["proportion_habitant_dans_avant_1945"]))
print("Corrélation de Spearman : ", spearmanr(filosofi["proportion_habitant_sup_80"], filosofi["proportion_habitant_dans_avant_1945"]))
Corrélation de Pearson : PearsonRResult(statistic=np.float64(0.30676528673616194), pvalue=np.float64(0.0)) Corrélation de Spearman : SignificanceResult(statistic=np.float64(0.2875337925247301), pvalue=np.float64(0.0))
On constate que la corrélation est bien moins forte ! On a encore une corrélation, encore positive et significative, mais plutôt faible (R=0.3)
1.3) Régression¶
On va essayer de prédire le niveau de vie en fonction du nombre de logements construit avant 1945.
var_3 = "log_av45"
var_4 = "ind_snv"
filosofi.plot(figsize=(10,10), kind="scatter", x=var_3, y=var_4)
<Axes: xlabel='log_av45', ylabel='ind_snv'>
Il semble exister un lien statistique entre ces deux variables, et il a l'air à peu près linéaire. Faire une regression semble donc pertinent.
from scipy.stats import linregress
reg = linregress(filosofi[var_3], filosofi[var_4])
print("R²:", (reg.rvalue)**2, "p-value:", reg.pvalue)
R²: 0.7085580778444482 p-value: 0.0
Le R² est assez élevé : le modèle linéaire prédit bien le niveau de vie. La p-value est très faible : on peut rejetter l'indépendance des variables avec une grande certitude.
var_3 = "log_av45"
var_4 = "ind_snv"
ax = filosofi.plot(figsize=(10,10), kind="scatter", x=var_3, y=var_4)
#Affichage de la droite de régression
plt.plot(
[filosofi[var_3].min(), filosofi[var_3].max()],
[reg.intercept + reg.slope * filosofi[var_3].min(), reg.intercept + reg.slope * filosofi[var_3].max()],
color="red"
)
[<matplotlib.lines.Line2D at 0x7ff28e11b140>]
2) Analyse bivariée entre deux variables qualitatives¶
Les distributions de CODE_CS et CODE_US sont-elles indépendantes ?
ocs_ge = gpd.read_file("OCCUPATION_SOL.shp")
ocs_ge.plot(figsize=(10,10), column="CODE_CS", legend=True)
ocs_ge.plot(figsize=(10,10), column="CODE_US", legend=True)
<Axes: >
On affiche la table de contingence entre couverture et usage du sol :
effectifs_observes = pd.crosstab(ocs_ge["CODE_CS"], ocs_ge["CODE_US"])
effectifs_observes
| CODE_US | US1.1 | US1.2 | US1.3 | US1.4 | US2 | US235 | US3 | US4.1.1 | US4.1.2 | US4.1.3 | US4.1.4 | US4.2 | US4.3 | US5 | US6.1 | US6.2 | US6.3 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CODE_CS | |||||||||||||||||
| CS1.1.1.1 | 13113 | 0 | 82 | 8 | 4024 | 48 | 10954 | 49 | 133 | 0 | 1 | 19 | 361 | 114534 | 346 | 89 | 0 |
| CS1.1.1.2 | 597 | 0 | 8 | 4 | 1672 | 6 | 5555 | 33942 | 145 | 5 | 1 | 13 | 181 | 12490 | 6 | 0 | 0 |
| CS1.1.2.1 | 4329 | 0 | 98 | 9 | 1051 | 32 | 2549 | 84 | 1956 | 0 | 0 | 4 | 98 | 2687 | 483 | 2 | 0 |
| CS1.2.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 362 |
| CS1.2.2 | 12 | 0 | 38 | 11 | 26 | 0 | 157 | 5 | 0 | 0 | 11 | 0 | 73 | 157 | 0 | 0 | 2696 |
| CS2.1.1.1 | 259 | 10879 | 40 | 0 | 483 | 3 | 2237 | 332 | 0 | 0 | 0 | 0 | 0 | 13739 | 0 | 0 | 13656 |
| CS2.1.1.2 | 0 | 1701 | 1 | 0 | 3 | 1 | 41 | 1 | 0 | 0 | 0 | 0 | 0 | 104 | 0 | 0 | 141 |
| CS2.1.1.3 | 0 | 2101 | 1 | 0 | 16 | 0 | 143 | 9 | 0 | 0 | 0 | 0 | 0 | 733 | 0 | 0 | 351 |
| CS2.1.2 | 178 | 4 | 26 | 0 | 18 | 0 | 69 | 54 | 80 | 0 | 0 | 0 | 339 | 93 | 2 | 0 | 2917 |
| CS2.1.3 | 571 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 0 | 0 | 0 |
| CS2.2.1 | 19263 | 0 | 95 | 7 | 2111 | 8 | 6640 | 1035 | 221 | 10 | 0 | 21 | 598 | 43590 | 113 | 0 | 4700 |
On peut directement réaliser le test avec la fonction chi2_contingency de scipy.
from scipy.stats import chi2_contingency
chi2, p, ddl, expected = chi2_contingency(effectifs_observes)
print("\n--- Résultats du Test du Chi2 ---")
print(f"Statistique du Chi2 (χ²): {chi2:.4f}")
print(f"p-valeur (p): {p:.4f}")
print(f"Degrés de liberté (ddl): {ddl}")
--- Résultats du Test du Chi2 --- Statistique du Chi2 (χ²): 525035.8906 p-valeur (p): 0.0000 Degrés de liberté (ddl): 160
La p-valeur est très faible (inférieure à $10^{-5}$). On peut donc rejetter l'hypothèse d'indépendance avec une certitude importante.
On peut aussi faire toutes les étapes du Chi2 :
frequences_observes = effectifs_observes/ocs_ge.shape[0]
frequences_observes
| CODE_US | US1.1 | US1.2 | US1.3 | US1.4 | US2 | US235 | US3 | US4.1.1 | US4.1.2 | US4.1.3 | US4.1.4 | US4.2 | US4.3 | US5 | US6.1 | US6.2 | US6.3 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CODE_CS | |||||||||||||||||
| CS1.1.1.1 | 0.038002 | 0.000000 | 0.000238 | 0.000023 | 0.011662 | 0.000139 | 0.031745 | 0.000142 | 0.000385 | 0.000000 | 0.000003 | 0.000055 | 0.001046 | 0.331926 | 0.001003 | 0.000258 | 0.000000 |
| CS1.1.1.2 | 0.001730 | 0.000000 | 0.000023 | 0.000012 | 0.004846 | 0.000017 | 0.016099 | 0.098366 | 0.000420 | 0.000014 | 0.000003 | 0.000038 | 0.000525 | 0.036197 | 0.000017 | 0.000000 | 0.000000 |
| CS1.1.2.1 | 0.012546 | 0.000000 | 0.000284 | 0.000026 | 0.003046 | 0.000093 | 0.007387 | 0.000243 | 0.005669 | 0.000000 | 0.000000 | 0.000012 | 0.000284 | 0.007787 | 0.001400 | 0.000006 | 0.000000 |
| CS1.2.1 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.001049 |
| CS1.2.2 | 0.000035 | 0.000000 | 0.000110 | 0.000032 | 0.000075 | 0.000000 | 0.000455 | 0.000014 | 0.000000 | 0.000000 | 0.000032 | 0.000000 | 0.000212 | 0.000455 | 0.000000 | 0.000000 | 0.007813 |
| CS2.1.1.1 | 0.000751 | 0.031528 | 0.000116 | 0.000000 | 0.001400 | 0.000009 | 0.006483 | 0.000962 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.039816 | 0.000000 | 0.000000 | 0.039576 |
| CS2.1.1.2 | 0.000000 | 0.004930 | 0.000003 | 0.000000 | 0.000009 | 0.000003 | 0.000119 | 0.000003 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000301 | 0.000000 | 0.000000 | 0.000409 |
| CS2.1.1.3 | 0.000000 | 0.006089 | 0.000003 | 0.000000 | 0.000046 | 0.000000 | 0.000414 | 0.000026 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.002124 | 0.000000 | 0.000000 | 0.001017 |
| CS2.1.2 | 0.000516 | 0.000012 | 0.000075 | 0.000000 | 0.000052 | 0.000000 | 0.000200 | 0.000156 | 0.000232 | 0.000000 | 0.000000 | 0.000000 | 0.000982 | 0.000270 | 0.000006 | 0.000000 | 0.008454 |
| CS2.1.3 | 0.001655 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000014 | 0.000000 | 0.000000 | 0.000000 |
| CS2.2.1 | 0.055825 | 0.000000 | 0.000275 | 0.000020 | 0.006118 | 0.000023 | 0.019243 | 0.002999 | 0.000640 | 0.000029 | 0.000000 | 0.000061 | 0.001733 | 0.126326 | 0.000327 | 0.000000 | 0.013621 |
frequences_usages = frequences_observes.sum(axis=0)
display(frequences_usages)
frequences_couvertures = frequences_observes.sum(axis=1)
display(frequences_couvertures)
| 0 | |
|---|---|
| CODE_US | |
| US1.1 | 0.111059 |
| US1.2 | 0.042558 |
| US1.3 | 0.001127 |
| US1.4 | 0.000113 |
| US2 | 0.027253 |
| US235 | 0.000284 |
| US3 | 0.082145 |
| US4.1.1 | 0.102913 |
| US4.1.2 | 0.007347 |
| US4.1.3 | 0.000043 |
| US4.1.4 | 0.000038 |
| US4.2 | 0.000165 |
| US4.3 | 0.004782 |
| US5 | 0.545217 |
| US6.1 | 0.002753 |
| US6.2 | 0.000264 |
| US6.3 | 0.071938 |
| 0 | |
|---|---|
| CODE_CS | |
| CS1.1.1.1 | 0.416627 |
| CS1.1.1.2 | 0.158306 |
| CS1.1.2.1 | 0.038782 |
| CS1.2.1 | 0.001049 |
| CS1.2.2 | 0.009233 |
| CS2.1.1.1 | 0.120640 |
| CS2.1.1.2 | 0.005776 |
| CS2.1.1.3 | 0.009720 |
| CS2.1.2 | 0.010955 |
| CS2.1.3 | 0.001669 |
| CS2.2.1 | 0.227242 |
frequences_theoriques = pd.DataFrame(
frequences_couvertures.to_numpy().reshape(-1,1).dot(frequences_usages.to_numpy().reshape(1,-1)),
index = frequences_couvertures.index,
columns = frequences_usages.index
)
frequences_theoriques
| CODE_US | US1.1 | US1.2 | US1.3 | US1.4 | US2 | US235 | US3 | US4.1.1 | US4.1.2 | US4.1.3 | US4.1.4 | US4.2 | US4.3 | US5 | US6.1 | US6.2 | US6.3 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CODE_CS | |||||||||||||||||
| CS1.1.1.1 | 0.046270 | 0.017731 | 0.000470 | 4.708895e-05 | 0.011354 | 1.183261e-04 | 0.034224 | 0.042876 | 0.003061 | 1.811113e-05 | 1.569632e-05 | 6.882231e-05 | 0.001992 | 0.227152 | 0.001147 | 1.098742e-04 | 0.029972 |
| CS1.1.1.2 | 0.017581 | 0.006737 | 0.000178 | 1.789243e-05 | 0.004314 | 4.496047e-05 | 0.013004 | 0.016292 | 0.001163 | 6.881704e-06 | 5.964143e-06 | 2.615048e-05 | 0.000757 | 0.086311 | 0.000436 | 4.174900e-05 | 0.011388 |
| CS1.1.2.1 | 0.004307 | 0.001650 | 0.000044 | 4.383277e-06 | 0.001057 | 1.101439e-05 | 0.003186 | 0.003991 | 0.000285 | 1.685876e-06 | 1.461092e-06 | 6.406328e-06 | 0.000185 | 0.021144 | 0.000107 | 1.022765e-05 | 0.002790 |
| CS1.2.1 | 0.000117 | 0.000045 | 0.000001 | 1.185732e-07 | 0.000029 | 2.979531e-07 | 0.000086 | 0.000108 | 0.000008 | 4.560507e-08 | 3.952439e-08 | 1.732993e-07 | 0.000005 | 0.000572 | 0.000003 | 2.766707e-07 | 0.000075 |
| CS1.2.2 | 0.001025 | 0.000393 | 0.000010 | 1.043575e-06 | 0.000252 | 2.622317e-06 | 0.000758 | 0.000950 | 0.000068 | 4.013750e-07 | 3.478583e-07 | 1.525225e-06 | 0.000044 | 0.005034 | 0.000025 | 2.435008e-06 | 0.000664 |
| CS2.1.1.1 | 0.013398 | 0.005134 | 0.000136 | 1.363526e-05 | 0.003288 | 3.426296e-05 | 0.009910 | 0.012415 | 0.000886 | 5.244331e-06 | 4.545087e-06 | 1.992846e-05 | 0.000577 | 0.065775 | 0.000332 | 3.181561e-05 | 0.008679 |
| CS2.1.1.2 | 0.000641 | 0.000246 | 0.000007 | 6.528076e-07 | 0.000157 | 1.640388e-06 | 0.000474 | 0.000594 | 0.000042 | 2.510798e-07 | 2.176025e-07 | 9.541034e-07 | 0.000028 | 0.003149 | 0.000016 | 1.523218e-06 | 0.000416 |
| CS2.1.1.3 | 0.001080 | 0.000414 | 0.000011 | 1.098603e-06 | 0.000265 | 2.760593e-06 | 0.000798 | 0.001000 | 0.000071 | 4.225398e-07 | 3.662011e-07 | 1.605651e-06 | 0.000046 | 0.005300 | 0.000027 | 2.563408e-06 | 0.000699 |
| CS2.1.2 | 0.001217 | 0.000466 | 0.000012 | 1.238140e-06 | 0.000299 | 3.111223e-06 | 0.000900 | 0.001127 | 0.000080 | 4.762076e-07 | 4.127133e-07 | 1.809589e-06 | 0.000052 | 0.005973 | 0.000030 | 2.888993e-06 | 0.000788 |
| CS2.1.3 | 0.000185 | 0.000071 | 0.000002 | 1.886689e-07 | 0.000045 | 4.740911e-07 | 0.000137 | 0.000172 | 0.000012 | 7.256497e-08 | 6.288964e-08 | 2.757469e-07 | 0.000008 | 0.000910 | 0.000005 | 4.402275e-07 | 0.000120 |
| CS2.2.1 | 0.025237 | 0.009671 | 0.000256 | 2.568387e-05 | 0.006193 | 6.453895e-05 | 0.018667 | 0.023386 | 0.001669 | 9.878411e-06 | 8.561289e-06 | 3.753796e-05 | 0.001087 | 0.123896 | 0.000626 | 5.992902e-05 | 0.016347 |
effectifs_theoriques = frequences_theoriques * ocs_ge.shape[0]
effectifs_theoriques
| CODE_US | US1.1 | US1.2 | US1.3 | US1.4 | US2 | US235 | US3 | US4.1.1 | US4.1.2 | US4.1.3 | US4.1.4 | US4.2 | US4.3 | US5 | US6.1 | US6.2 | US6.3 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CODE_CS | |||||||||||||||||
| CS1.1.1.1 | 15965.991445 | 6118.171921 | 162.068020 | 16.248465 | 3917.963143 | 40.829476 | 11809.300859 | 14794.852101 | 1056.150209 | 6.249410 | 5.416155 | 23.747756 | 687.435047 | 78380.927470 | 395.795936 | 37.913084 | 10341.939503 |
| CS1.1.1.2 | 6066.612521 | 2324.727438 | 61.581135 | 6.173944 | 1488.712075 | 15.514014 | 4487.190959 | 5621.613623 | 401.306371 | 2.374594 | 2.057981 | 9.023457 | 261.205330 | 29782.473432 | 150.390948 | 14.405870 | 3929.636309 |
| CS1.1.2.1 | 1486.195126 | 569.510345 | 15.086110 | 1.512489 | 364.703798 | 3.800614 | 1099.269371 | 1377.179561 | 98.311796 | 0.581727 | 0.504163 | 2.210561 | 63.989926 | 7296.092622 | 36.842685 | 3.529141 | 962.679965 |
| CS1.2.1 | 40.203455 | 15.405974 | 0.408098 | 0.040915 | 9.865698 | 0.102811 | 29.736625 | 37.254446 | 2.659458 | 0.015736 | 0.013638 | 0.059798 | 1.731008 | 197.368520 | 0.996641 | 0.095468 | 26.041709 |
| CS1.2.2 | 353.834828 | 135.589595 | 3.591716 | 0.360095 | 86.829047 | 0.904854 | 261.715156 | 327.880293 | 23.406171 | 0.138498 | 0.120032 | 0.526293 | 15.234786 | 1737.061059 | 8.771543 | 0.840222 | 229.195813 |
| CS2.1.1.1 | 4623.175214 | 1771.601900 | 46.929053 | 4.704969 | 1134.500801 | 11.822743 | 3419.547556 | 4284.055504 | 305.823004 | 1.809604 | 1.568323 | 6.876494 | 199.056393 | 22696.289319 | 114.608226 | 10.978262 | 2994.652636 |
| CS2.1.1.2 | 221.341121 | 84.817973 | 2.246795 | 0.225257 | 54.315847 | 0.566031 | 163.715727 | 205.105281 | 14.641713 | 0.086637 | 0.075086 | 0.329222 | 9.530109 | 1086.617292 | 5.487033 | 0.525600 | 143.373275 |
| CS2.1.1.3 | 372.492785 | 142.739329 | 3.781110 | 0.379083 | 91.407603 | 0.952568 | 275.515578 | 345.169649 | 24.640395 | 0.145801 | 0.126361 | 0.554044 | 16.038127 | 1828.657499 | 9.234073 | 0.884527 | 241.281468 |
| CS2.1.2 | 419.804034 | 160.869011 | 4.261358 | 0.427231 | 103.017513 | 1.073556 | 310.509507 | 389.010517 | 27.770034 | 0.164320 | 0.142410 | 0.624415 | 18.075170 | 2060.919901 | 10.406916 | 0.996873 | 271.927236 |
| CS2.1.3 | 63.970138 | 24.513373 | 0.649350 | 0.065102 | 15.697907 | 0.163589 | 47.315734 | 59.277793 | 4.231624 | 0.025039 | 0.021701 | 0.095149 | 2.754312 | 314.044937 | 1.585816 | 0.151904 | 41.436531 |
| CS2.2.1 | 8708.379332 | 3337.053142 | 88.397254 | 8.862450 | 2136.986568 | 22.269745 | 6441.182928 | 8069.601233 | 576.059225 | 3.408634 | 2.954150 | 12.952811 | 374.949791 | 42751.547950 | 215.880183 | 20.679049 | 5640.835556 |
test_chi2 = ((effectifs_observes - effectifs_theoriques)**2/effectifs_theoriques).sum().sum()
test_chi2
np.float64(525035.8906068673)
from scipy.stats import chi2
degre_de_liberte = (len(frequences_usages)-1) * (len(frequences_couvertures)-1)
p_value = 0.01
valeur_critique = chi2.ppf(1 - p_value, degre_de_liberte)
print(f"La valeur critique du chi2 pour un degré de liberté={degre_de_liberte} et une p-value={p_value} est : {valeur_critique}")
La valeur critique du chi2 pour un degré de liberté=160 et une p-value=0.01 est : 204.5300945903855
La valeur obtenue par le test du Chi² est bien supérieure à la valeur critique. On peut donc rejetter l'indépendance des deux variables, avec moins d'1% de chance de se tromper.
Analyse bivariée entre une variable qualitative et une variable quantitative¶
On va d'abord restreindre le fichier filosofi au département de l'Ain. On va utiliser le code des communes pour faire cette selection facilement
filosofi_ain = filosofi[filosofi["lcog_geo"].str.startswith("01")]
#Ce qu'il y a entre les crochets est une pd.Series de Booleen "True" quand le carreaux est dans l'Ain et False sinon.
#La syntaxe df[booleen_serie] permet de renvoyer le sous ensemble du dataframe initial avec tous les indices pour lesquels booleen_serie est "True"
filosofi_ain.plot(column="ind", legend=True)
<Axes: >
On va réaliser l'intersection entre les deux couches
intersection = ocs_ge.overlay(filosofi_ain,
how="intersection",
keep_geom_type=True
)
Chaque polygone de cette intersection a un usage bien défini. On va désagréger la population à l'échelle de ces unités cartographiques en faisant l'hypothèse que la population est répartie de manière homogène dans les carreaux de 1 km.
intersection["ind_estimee"] = intersection["ind"] * (intersection.area/1000000)
intersection.head()
| ID | CODE_CS | CODE_US | MILLESIME | SOURCE | OSSATURE | ID_ORIGINE | CODE_OR | idcar_1km | i_est_1km | ... | ind_25_39 | ind_40_54 | ind_55_64 | ind_65_79 | ind_80p | ind_inc | proportion_habitant_sup_80 | proportion_habitant_dans_avant_1945 | geometry | ind_estimee | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | OCSGE0000000010019471939 | CS1.1.1.1 | US5 | 2021 | calcul | 0 | NC | NC | CRS3035RES1000mN2580000E4012000 | 0 | ... | 6.0 | 31.0 | 7.0 | 11.0 | 2.1 | 0.0 | 0.023204 | 0.000000 | POLYGON ((930611.6 6577732.26, 930609.6 657773... | 0.029604 |
| 1 | OCSGE0000000010019471929 | CS1.1.1.1 | US5 | 2021 | calcul | 0 | NC | NC | CRS3035RES1000mN2580000E4011000 | 1 | ... | 41.8 | 66.0 | 34.5 | 47.7 | 13.9 | 0.0 | 0.047039 | 0.019966 | POLYGON ((930397.1 6577673.9, 930388.11 657768... | 0.128211 |
| 2 | OCSGE0000000010019471930 | CS1.1.1.1 | US5 | 2021 | calcul | 0 | NC | NC | CRS3035RES1000mN2580000E4011000 | 1 | ... | 41.8 | 66.0 | 34.5 | 47.7 | 13.9 | 0.0 | 0.047039 | 0.019966 | POLYGON ((929937.96 6577691.9, 929926.31 65777... | 0.059096 |
| 3 | OCSGE0000000010019471931 | CS1.1.1.1 | US5 | 2021 | calcul | 0 | NC | NC | CRS3035RES1000mN2580000E4012000 | 0 | ... | 6.0 | 31.0 | 7.0 | 11.0 | 2.1 | 0.0 | 0.023204 | 0.000000 | POLYGON ((930752.74 6577721, 930766.5 6577715.... | 0.021780 |
| 4 | OCSGE0000000010019471932 | CS1.1.1.1 | US5 | 2021 | calcul | 0 | NC | NC | CRS3035RES1000mN2580000E4011000 | 1 | ... | 41.8 | 66.0 | 34.5 | 47.7 | 13.9 | 0.0 | 0.047039 | 0.019966 | POLYGON ((930382.8 6577714.3, 930394.16 657772... | 0.062773 |
5 rows × 43 columns
intersection["ind_estimee"].head()
| ind_estimee | |
|---|---|
| 0 | 0.029604 |
| 1 | 0.128211 |
| 2 | 0.059096 |
| 3 | 0.021780 |
| 4 | 0.062773 |
Certains morceaux n'ont pas été intersectés. Ils ont pour l'instant une valeur "ind_estimee": NaN. Pour notre analyse, on va considérer qu'ils ont une population nulle.
intersection["ind_estimee"] = intersection["ind_estimee"].fillna(0)
On va regrouper au niveau des unités cartographiques initiales de l'OCS GE. Pour cela, on va faire la somme des populations estimées par ID de l'OCS GE.
pop_par_ocsge = intersection.groupby("ID")["ind_estimee"].sum()
ocs_ge = ocs_ge.set_index("ID")
ocs_ge["ind_estimee"] = pop_par_ocsge
ocs_ge.plot(kind="hist", column="ind_estimee", bins=100)
<Axes: ylabel='Frequency'>
L'histogramme montre qu'une très grande majorité des polygones a une population très faible, et que la distribution est très asymétrique. On va donc faire une tranformation logarithmique
ocs_ge["log_ind_estimee"] = np.log10(ocs_ge["ind_estimee"])
ocs_ge.plot(kind="hist", column="log_ind_estimee", bins=100)
<Axes: ylabel='Frequency'>
ocs_ge.boxplot(column="log_ind_estimee", by="CODE_US", figsize=(10,10), rot=90)
<Axes: title={'center': 'log_ind_estimee'}, xlabel='CODE_US'>
On constate que les distributions des populations sont différentes selon les usages : il semble exister un lien entre les deux. Pour autant, le modèle qu'on a utilisé pour la désagrégation de la population ne semble pas très pertinent : l'usage avec la plus haute population médiane est l'usage "Navigable" ! D'autre part les unités spatiales ont des tailles très variées, et comparer directement leur population n'est pas si pertinent.
ocs_ge["densite"] = ocs_ge["ind_estimee"]/ocs_ge.area *1000000 #(habitants/km²)
ocs_ge.plot(kind="hist", column="densite", bins=100)
ocs_ge.boxplot(column="densite", by="CODE_US", figsize=(10,10), rot=90)
<Axes: title={'center': 'densite'}, xlabel='CODE_US'>
ocs_ge["log_densite"] = np.log10(ocs_ge["densite"])
ocs_ge.plot(kind="hist", column="log_densite", bins=100)
ocs_ge.boxplot(column="log_densite", by="CODE_US", figsize=(10,10), rot=90)
<Axes: title={'center': 'log_densite'}, xlabel='CODE_US'>
On va essayer de faire l'inverse : plutôt que de calculer la population des polygones de l'OCS GE, on va chercher l'usage majoritaire des carreaux.
intersection["intersected_area"] = intersection.area
grouped = intersection.groupby(by=["idcar_1km", "CODE_US"])
area_per_US = grouped.aggregate({"intersected_area": "sum"})
area_per_US
| intersected_area | ||
|---|---|---|
| idcar_1km | CODE_US | |
| CRS3035RES1000mN2510000E3979000 | US1.1 | 199509.974876 |
| US1.2 | 247506.319747 | |
| US3 | 148022.904047 | |
| US4.1.1 | 8643.642790 | |
| US5 | 26332.931045 | |
| ... | ... | ... |
| CRS3035RES1000mN2613000E3932000 | US1.2 | 72979.564333 |
| US3 | 6874.732000 | |
| US4.1.1 | 5182.361158 | |
| US5 | 2594.768041 | |
| US6.3 | 94273.165015 |
25388 rows × 1 columns
def main_us(groupe):
#On définit une fonction d'aggrégation qu'on va appliquer à chaque groupe de carreaux, et qui va retourner l'usage avec la plus grande surface
return groupe["CODE_US"].iloc[np.argmax(groupe["intersected_area"])]
usage_majoritaire = area_per_US.reset_index().groupby("idcar_1km").apply(main_us, include_groups=False)
filosofi_ain = filosofi_ain.set_index("idcar_1km")
filosofi_ain["Usage majoritaire"] = usage_majoritaire
filosofi_ain.plot(column="Usage majoritaire", legend=True)
<Axes: >
filosofi_ain.boxplot(column="ind", by="Usage majoritaire", figsize=(10,10), rot=90)
<Axes: title={'center': 'ind'}, xlabel='Usage majoritaire'>
Ça semble plus crédible
La carte permet de constater une limite à cette approche : les trous correspondent à "des carreaux inhabités", mais qui ont pourtant un usage.