Rapport individus

This commit is contained in:
2025-10-18 11:12:09 +02:00
parent 4e61fccf48
commit 27a2e99706
3 changed files with 233 additions and 114 deletions

View File

@@ -76,9 +76,11 @@ function randomSex(age:number) {
return Math.random() < c ? "F" : "M";
}
function randomSport(age:number) {
function randomSport(sexe: "M" | "F", age:number) {
// Moyenne de sport qui diminue avec lâge
const meanSport = Math.min(Math.max(0.65 - 0.006 * (age - 18), 0.05), 0.9);
let meanSport = Math.min(Math.max(0.65 - 0.0065 * (age - 18), 0.05), 0.9);
if (sexe === "F") meanSport -= 0.15;
// Paramètres de la distribution bêta
const a = Math.max(meanSport * 6, 0.5);
@@ -130,9 +132,18 @@ function randomWealth(education:number) {
return wealth;
}
function randomLecture(education:number, age:number) {
function randomLecture(sexe: "M" | "F", education:number, age:number) {
// Base : 15% + effet de l'éducation + effet de l'âge
let meanRead = 0.15 + 0.18 * education + 0.002 * (age - 18);
meanRead = Math.min(Math.max(meanRead, 0.02), 0.98); // équivalent de np.clip
// Femmes plus lectrices : ajout dun bonus
if (sexe === "F") meanRead += 0.16;
// Bonus sénior : lecture plus fréquente chez les séniors
if (age > 60) meanRead += 0.09;
// Clipping entre 0.02 et 0.98
meanRead = Math.min(Math.max(meanRead, 0.02), 0.98);
// Paramètres de la distribution bêta
const a = Math.max(meanRead * 7, 0.5);
@@ -144,6 +155,10 @@ function randomLecture(education:number, age:number) {
return reading;
}
/**
* Musique pratique indépendante de l'âge
* @returns valeur normalisée (0-1)
*/
function randomMusique() {
return jStat.beta.sample(2, 2);
}
@@ -158,8 +173,8 @@ export function generate(n: number) {
const etudes = randomEducation(age);
const richesse = randomWealth(etudes);
const sport = randomSport(age);
const lecture = randomLecture(etudes, age);
const sport = randomSport(sexe, age);
const lecture = randomLecture(sexe, etudes, age);
const musique = randomMusique();
individus.push({ id: i, prenom, sexe, age, etudes, richesse, sport, lecture, musique });

View File

@@ -2,12 +2,15 @@ import './style.css';
import { generate } from "./individual";
import Chart from "chart.js/auto";
import ChartDataLabels from "chartjs-plugin-datalabels";
import { jStat } from "jstat";
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { jStat } from "jstat";
const individus = generate(1000);
const individualMale = individus.filter(i => i.sexe === "M");
const individualFemale = individus.filter(i => i.sexe === "F");
// Fonction utilitaire pour afficher les pourcentages
const percentage = (value: number, total: number) => ((value / total) * 100).toFixed(1) + "%";
const percentage = (value: number, total: number) => ((value / total) * 100).toFixed(0) + "%";
function formatStats(ages: number[]) {
return {
@@ -20,10 +23,16 @@ function formatStats(ages: number[]) {
};
}
function histogramData(ages: number[]) {
return ageClasses.map((a, idx) =>
ages.filter(age => age >= a && age < (ageClasses[idx + 1] ?? 80)).length
);
}
const Ages:number[] = individus.map(i => i.age);
// Séparer les âges par sexe
const AgesH = individus.filter(i => i.sexe === "M").map(i => i.age);
const AgesF = individus.filter(i => i.sexe === "F").map(i => i.age);
const AgesH = individualMale.map(i => i.age);
const AgesF = individualFemale.map(i => i.age);
const stats = formatStats(Ages);
for (const [key, value] of Object.entries(stats)) {
@@ -33,11 +42,6 @@ for (const [key, value] of Object.entries(stats)) {
// Construire des classes d'âge
const ageClasses = Array.from({ length: 20 }, (_, i) => i * 3 + 18);
function histogramData(ages: number[]) {
return ageClasses.map((a, idx) =>
ages.filter(age => age >= a && age < (ageClasses[idx + 1] ?? 80)).length
);
}
Chart.register(ChartDataLabels,MatrixController, MatrixElement);
@@ -46,6 +50,59 @@ Chart.defaults.set('plugins.datalabels', {
font: { weight: "bold" }
});
/**
* Doughnut Sexe
*/
const nbMales = individualMale.length;
const nbFemales = individus.length - nbMales;
new Chart(document.getElementById("genreChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
labels: ["Hommes", "Femmes"],
datasets: [{
data: [nbMales, nbFemales],
backgroundColor: ["#4A90E2", "#FF69B4"],
}],
},
options: {
plugins: { legend: { position: "bottom" },
datalabels: {
formatter: (value, context) => {
const total = context.chart.data.datasets[0].data.reduce((a: number, b: number) => a + b, 0);
return percentage(value, total);
}
}
},
},
});
/**
* Doughnut Classes d'âges
*/
const classes = {
Jeune: individus.filter(i => i.age <= 30),
Adulte: individus.filter(i => i.age > 30 && i.age <= 60),
Senior: individus.filter(i => i.age > 60)
};
new Chart(document.getElementById("classeChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
labels: ["Jeunes (≤30)", "Adultes (3160)", "Seniors (>60)"],
datasets: [{
data: [classes.Jeune.length, classes.Adulte.length, classes.Senior.length],
backgroundColor: ["#81C784", "#FFD54F", "#E57373"],
}],
},
options: {
plugins: { legend: { position: "bottom" } },
},
});
/**
* Histogramme des ages
*/
new Chart(document.getElementById("ageChart") as HTMLCanvasElement, {
type: "bar",
data: {
@@ -64,7 +121,7 @@ new Chart(document.getElementById("ageChart") as HTMLCanvasElement, {
],
},
options: {
responsive: true,
responsive: false,
plugins: {
title: { display: true, text: "Distribution des âges par sexe" },
datalabels: false
@@ -76,41 +133,21 @@ new Chart(document.getElementById("ageChart") as HTMLCanvasElement, {
},
});
const hommes = individus.filter(i => i.sexe === "M").length;
const femmes = individus.length - hommes;
new Chart(document.getElementById("genreChart") as HTMLCanvasElement, {
/**
* Éducation
*/
new Chart(document.getElementById("educationChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
labels: ["Hommes", "Femmes"],
labels: ["Bac", "+2", "+3", "+5"],
datasets: [{
data: [hommes, femmes],
backgroundColor: ["#4A90E2", "#FF69B4"],
}],
},
options: {
plugins: { legend: { position: "bottom" },
datalabels: {
formatter: (value, context) => {
const total = context.chart.data.datasets[0].data.reduce((a: number, b: number) => a + b, 0);
return percentage(value, total);
}
}
},
},
});
const jeunes = individus.filter(i => i.age <= 30).length;
const adultes = individus.filter(i => i.age > 30 && i.age <= 60).length;
const seniors = individus.filter(i => i.age > 60).length;
new Chart(document.getElementById("classeChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
labels: ["Jeunes (≤30)", "Adultes (3160)", "Seniors (>60)"],
datasets: [{
data: [jeunes, adultes, seniors],
backgroundColor: ["#81C784", "#FFD54F", "#E57373"],
data: [ individus.filter(i => i.etudes == 0).length,
individus.filter(i => i.etudes == 1).length,
individus.filter(i => i.etudes == 2).length,
individus.filter(i => i.etudes == 3).length,],
backgroundColor: ["#55e0d9ff", "#81C784", "#FFD54F", "#E57373"],
}],
},
options: {
@@ -118,6 +155,9 @@ new Chart(document.getElementById("classeChart") as HTMLCanvasElement, {
},
});
/**
* Richesse
*/
new Chart(document.getElementById("wealthChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
@@ -140,92 +180,156 @@ new Chart(document.getElementById("wealthChart") as HTMLCanvasElement, {
},
});
new Chart(document.getElementById("educationChart") as HTMLCanvasElement, {
type: "doughnut",
data: {
labels: ["Bac", "+2", "+3", "+5"],
datasets: [{
data: [ individus.filter(i => i.etudes == 0).length,
individus.filter(i => i.etudes == 1).length,
individus.filter(i => i.etudes == 2).length,
individus.filter(i => i.etudes == 3).length,],
backgroundColor: ["#81C784", "#FFD54F", "#E57373"],
}],
},
options: {
plugins: { legend: { position: "bottom" } },
},
/**
* HeatMap
*/
const activites = ["Lecture", "Musique", "Sport"];
const ages = Array.from({ length: 74 - 18 + 1 }, (_, i) => i + 18);
// Labels Y : chaque activité doublée pour H/F
const yLabels = activites.flatMap(a => [`${a} H`, `${a} F`]);
// Génération des données
const dataHeatmap = ages.flatMap((age, x) => {
const window = individus.filter(i => i.age >= age - 1 && i.age <= age + 1); // moyenne glissante
// Pour chaque activité
return activites.flatMap((activite, y) => {
// map pour extraire les valeurs
// const valeurs = window.map(i => i[activite as keyof typeof i]);
const hommes = window.filter(i => i.sexe === 'M');
const femmes = window.filter(i => i.sexe === 'F');
// reduce pour la moyenne
const moyenneH = hommes.length > 0
? hommes.map(i => i[activite.toLowerCase() as keyof typeof i])
.reduce((sum, val) => sum + val, 0) / hommes.length
: 0;
const moyenneF = femmes.length > 0
? femmes.map(i => i[activite.toLowerCase() as keyof typeof i])
.reduce((sum, val) => sum + val, 0) / femmes.length
: 0;
return [
{ x, y: y * 2, v: moyenneH },
{ x, y: y * 2 + 1, v: moyenneF }
];
});
});
new Chart(document.getElementById('heatmapChart') as HTMLCanvasElement, {
type: 'matrix',
new Chart(document.getElementById("heatmapChart") as HTMLCanvasElement, {
type: "matrix",
data: {
labels: individus.map((_, i) => `Individu ${i + 1}`),
datasets: [{
label: 'Activités',
data: individus.map((individu, i) => [
{ x: 0, y: i, v: individu.sport },
{ x: 1, y: i, v: individu.musique },
{ x: 2, y: i, v: individu.lecture }
]).flat(),
backgroundColor: ({ v }) => {
const r = Math.floor(255 - v * 100);
const g = Math.floor(255 - v * 50);
const b = Math.floor(255);
return `rgb(${r}, ${g}, ${b})`;
},
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.6)',
width: ({ chart }) => (chart.chartArea?.width ?? 0) / 3 - 1,
height: ({ chart }) => (chart.chartArea?.height ?? 0) / individus.length - 1
}]
datasets: [
{
label: 'Activités par age et par sexe',
data: dataHeatmap,
borderWidth: 0,
backgroundColor: ({ raw }: { raw: any }) => {
const mean = 0.5; // ou calculé dynamiquement
const contrastFactor = 2; // augmente les écarts
let v = (raw.v - mean) * contrastFactor + mean;
v = Math.min(Math.max(v, 0), 1); // clip sur [0,1]
const light = 90; // OKLCH clair
const dark = 40; // OKLCH foncé
const c = Math.floor(light - (light - dark) * v);
return `oklch(${c}% 0.12 145)`; // vert
},
width: ({ chart }: { chart: any }) =>
(chart.chartArea?.width ?? 0) / ages.length,
height: ({ chart }: { chart: any }) =>
(chart.chartArea?.height ?? 0) / yLabels.length - 1
}
]
},
options: {
responsive: true,
scales: {
x: {
type: 'category',
labels: ['Sport', 'Musique', 'Lecture'],
offset: true
},
y: {
type: 'category',
labels: individus.map((_, i) => `Individu ${i + 1}`),
offset: true
}
},
plugins: {
legend: { display: false },
datalabels: false,
tooltip: {
callbacks: {
label: ({ raw }) => `Valeur : ${raw.v.toFixed(2)}`
title: (items) => {
const raw = (items[0] as any).raw;
return `${yLabels[raw.y]} - ${ages[raw.x]} ans`;
},
label: ({ raw }: { raw: any }) =>
` ${(raw.v * 100).toFixed(0)}%`
}
}
},
scales: {
x: {
type: 'linear',
min: -0.5,
max: ages.length - 0.5,
ticks: {
autoSkip: false,
callback: (_, i) => i % 5 === 0 ? ages[i] ?? '' : '',
stepSize: 1
},
offset: false,
grid: { display: false }
},
y: {
type: 'linear',
min: -0.5,
max: yLabels.length - 0.5,
position: 'left',
ticks: {
callback: (_, i) => yLabels[i] ?? '',
crossAlign: 'near',
align: 'end',
stepSize: 1
},
offset: false,
grid: { display: false },
reverse: false
}
}
}
});
const datasets = Object.entries(classes).map(([label, group], idx) => {
const colors = [
"rgba(75,192,192,0.2)",
"rgba(255,99,132,0.2)",
"rgba(255,206,86,0.2)"
];
const borderColors = [
"rgba(75,192,192,1)",
"rgba(255,99,132,1)",
"rgba(255,206,86,1)"
];
return {
label,
data: [
jStat.mean(group.map(i => i.etudes)) / 3, // normalisation si besoin
jStat.mean(group.map(i => i.richesse)) / 3,
jStat.mean(group.map(i => i.sport)),
jStat.mean(group.map(i => i.musique)),
jStat.mean(group.map(i => i.lecture))
],
backgroundColor: colors[idx],
borderColor: borderColors[idx],
pointBackgroundColor: borderColors[idx]
};
});
new Chart(document.getElementById("radarChart") as HTMLCanvasElement, {
type: "radar",
data: {
labels: ["Etudes", "Richesse", "Sport", "Musique", "Lecture"],
datasets: [{
label: "Valeurs moyennes",
data: [
individus[10].etudes / 3,
individus[10].richesse / 3,
individus[10].sport,
individus[10].musique,
individus[10].lecture
],
backgroundColor: "rgba(54,162,235,0.2)",
borderColor: "rgba(54,162,235,1)",
pointBackgroundColor: "rgba(54,162,235,1)"
}]
labels: ["Études", "Richesse", "Sport", "Musique", "Lecture"],
datasets: datasets,
},
options: {
scales: { r: { min: 0, max: 1, ticks: { stepSize: 0.1 } } },
scales: { r: { min: 0, max: 1, ticks: { stepSize: 0.25 } } },
plugins: { legend: { position: "top" }, datalabels: false }
}
});