Auto-update: 2025-10-23 15:04:00

This commit is contained in:
divingeek 2025-10-23 15:04:00 +02:00
parent 7698240a79
commit 6170c48371
28 changed files with 19938 additions and 0 deletions

9
activite1/activite1.aux Normal file
View file

@ -0,0 +1,9 @@
\relax
\providecommand \babel@aux [2]{\global \let \babel@toc \@gobbletwo }
\@nameuse{bbl@beforestart}
\catcode `:\active
\catcode `;\active
\catcode `!\active
\catcode `?\active
\babel@aux{french}{}
\gdef \@abspage@last{4}

750
activite1/activite1.log Normal file
View file

@ -0,0 +1,750 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.27 (TeX Live 2026/dev/Arch Linux) (preloaded format=pdflatex 2025.8.2) 23 OCT 2025 12:21
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**activite1.tex
(./activite1.tex
LaTeX2e <2024-11-01> patch level 2
L3 programming layer <2025-01-18>
(/usr/share/texmf-dist/tex/latex/base/article.cls
Document Class: article 2024/06/29 v1.4n Standard LaTeX document class
(/usr/share/texmf-dist/tex/latex/base/size11.clo
File: size11.clo 2024/06/29 v1.4n Standard LaTeX file (size option)
)
\c@part=\count196
\c@section=\count197
\c@subsection=\count198
\c@subsubsection=\count199
\c@paragraph=\count266
\c@subparagraph=\count267
\c@figure=\count268
\c@table=\count269
\abovecaptionskip=\skip49
\belowcaptionskip=\skip50
\bibindent=\dimen141
)
(/usr/share/texmf-dist/tex/latex/base/inputenc.sty
Package: inputenc 2024/02/08 v1.3d Input encoding file
\inpenc@prehook=\toks17
\inpenc@posthook=\toks18
)
(/usr/share/texmf-dist/tex/latex/base/fontenc.sty
Package: fontenc 2021/04/29 v2.0v Standard LaTeX package
)
(/usr/share/texmf-dist/tex/generic/babel/babel.sty
Package: babel 2025/02/14 v25.4 The multilingual framework for pdfLaTeX, LuaLaT
eX and XeLaTeX
\babel@savecnt=\count270
\U@D=\dimen142
\l@unhyphenated=\language5
(/usr/share/texmf-dist/tex/generic/babel/txtbabel.def)
\bbl@readstream=\read2
\bbl@dirlevel=\count271
(/usr/share/texmf-dist/tex/generic/babel-french/francais.ldf
Language: francais 2024-07-25 v3.6c French support from the babel system
Package francais.ldf Warning: Option `francais' for Babel is *deprecated*,
(francais.ldf) it might be removed sooner or later. Please
(francais.ldf) use `french' instead; reported on input line 31.
(/usr/share/texmf-dist/tex/generic/babel-french/french.ldf
Language: french 2024-07-25 v3.6c French support from the babel system
Package babel Info: Hyphen rules for 'acadian' set to \l@french
(babel) (\language4). Reported on input line 91.
Package babel Info: Hyphen rules for 'canadien' set to \l@french
(babel) (\language4). Reported on input line 92.
\FB@stdchar=\count272
Package babel Info: Making : an active character on input line 421.
Package babel Info: Making ; an active character on input line 422.
Package babel Info: Making ! an active character on input line 423.
Package babel Info: Making ? an active character on input line 424.
\FBguill@level=\count273
\FBold@everypar=\toks19
\FB@Mht=\dimen143
\mc@charclass=\count274
\mc@charfam=\count275
\mc@charslot=\count276
\std@mcc=\count277
\dec@mcc=\count278
\FB@parskip=\dimen144
\listindentFB=\dimen145
\descindentFB=\dimen146
\labelindentFB=\dimen147
\labelwidthFB=\dimen148
\leftmarginFB=\dimen149
\parindentFFN=\dimen150
\FBfnindent=\dimen151
)))
(/usr/share/texmf-dist/tex/latex/carlisle/scalefnt.sty)
(/usr/share/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/share/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2022/05/29 v1.15 key=value parser (DPC)
\KV@toks@=\toks20
)
(/usr/share/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/share/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2024/12/12 v1.0g TeX engine tests
))
\Gm@cnth=\count279
\Gm@cntv=\count280
\c@Gm@tempcnt=\count281
\Gm@bindingoffset=\dimen152
\Gm@wd@mp=\dimen153
\Gm@odd@mp=\dimen154
\Gm@even@mp=\dimen155
\Gm@layoutwidth=\dimen156
\Gm@layoutheight=\dimen157
\Gm@layouthoffset=\dimen158
\Gm@layoutvoffset=\dimen159
\Gm@dimlist=\toks21
)
(/usr/share/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
(/usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
(/usr/share/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex
\pgfutil@everybye=\toks22
\pgfutil@tempdima=\dimen160
\pgfutil@tempdimb=\dimen161
)
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def
\pgfutil@abb=\box52
)
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
(/usr/share/texmf-dist/tex/generic/pgf/pgf.revision.tex)
Package: pgfrcs 2023-01-15 v3.1.10 (3.1.10)
))
Package: pgf 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
(/usr/share/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/share/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2024/08/06 v1.4g Standard LaTeX Graphics (DPC,SPQR)
(/usr/share/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2023/12/02 v1.11 sin cos tan (DPC)
)
(/usr/share/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 106.
(/usr/share/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2024/04/13 v1.2c Graphics/color driver for pdftex
))
\Gin@req@height=\dimen162
\Gin@req@width=\dimen163
)
(/usr/share/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
Package: pgfsys 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
\pgfkeys@pathtoks=\toks23
\pgfkeys@temptoks=\toks24
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeyslibraryfiltered.code.te
x
\pgfkeys@tmptoks=\toks25
))
\pgf@x=\dimen164
\pgf@y=\dimen165
\pgf@xa=\dimen166
\pgf@ya=\dimen167
\pgf@xb=\dimen168
\pgf@yb=\dimen169
\pgf@xc=\dimen170
\pgf@yc=\dimen171
\pgf@xd=\dimen172
\pgf@yd=\dimen173
\w@pgf@writea=\write3
\r@pgf@reada=\read3
\c@pgf@counta=\count282
\c@pgf@countb=\count283
\c@pgf@countc=\count284
\c@pgf@countd=\count285
\t@pgf@toka=\toks26
\t@pgf@tokb=\toks27
\t@pgf@tokc=\toks28
\pgf@sys@id@count=\count286
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg
File: pgf.cfg 2023-01-15 v3.1.10 (3.1.10)
)
Driver file for pgf: pgfsys-pdftex.def
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.def
File: pgfsys-pdftex.def 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def
File: pgfsys-common-pdf.def 2023-01-15 v3.1.10 (3.1.10)
)))
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
File: pgfsyssoftpath.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfsyssoftpath@smallbuffer@items=\count287
\pgfsyssoftpath@bigbuffer@items=\count288
)
(/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
File: pgfsysprotocol.code.tex 2023-01-15 v3.1.10 (3.1.10)
))
(/usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty
Package: xcolor 2024/09/29 v3.02 LaTeX color extensions (UK)
(/usr/share/texmf-dist/tex/latex/graphics-cfg/color.cfg
File: color.cfg 2016/01/02 v1.6 sample color configuration
)
Package xcolor Info: Driver file: pdftex.def on input line 274.
(/usr/share/texmf-dist/tex/latex/graphics/mathcolor.ltx)
Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1349.
Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1353.
Package xcolor Info: Model `RGB' extended on input line 1365.
Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1367.
Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1368.
Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1369.
Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1370.
Package xcolor Info: Model `Gray' substituted by `gray' on input line 1371.
Package xcolor Info: Model `wave' substituted by `hsb' on input line 1372.
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
Package: pgfcore 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex
\pgfmath@dimen=\dimen174
\pgfmath@count=\count289
\pgfmath@box=\box53
\pgfmath@toks=\toks29
\pgfmath@stack@operand=\toks30
\pgfmath@stack@operation=\toks31
)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric.code
.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.code.te
x) (/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithmetics
.code.tex) (/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex
\c@pgfmathroundto@lastzeros=\count290
))
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfint.code.tex)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex
File: pgfcorepoints.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@picminx=\dimen175
\pgf@picmaxx=\dimen176
\pgf@picminy=\dimen177
\pgf@picmaxy=\dimen178
\pgf@pathminx=\dimen179
\pgf@pathmaxx=\dimen180
\pgf@pathminy=\dimen181
\pgf@pathmaxy=\dimen182
\pgf@xx=\dimen183
\pgf@xy=\dimen184
\pgf@yx=\dimen185
\pgf@yy=\dimen186
\pgf@zx=\dimen187
\pgf@zy=\dimen188
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.code.tex
File: pgfcorepathconstruct.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@path@lastx=\dimen189
\pgf@path@lasty=\dimen190
) (/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.tex
File: pgfcorepathusage.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@shorten@end@additional=\dimen191
\pgf@shorten@start@additional=\dimen192
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex
File: pgfcorescopes.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfpic=\box54
\pgf@hbox=\box55
\pgf@layerbox@main=\box56
\pgf@picture@serial@count=\count291
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code.tex
File: pgfcoregraphicstate.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgflinewidth=\dimen193
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.code.t
ex
File: pgfcoretransformations.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@pt@x=\dimen194
\pgf@pt@y=\dimen195
\pgf@pt@temp=\dimen196
) (/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex
File: pgfcorequick.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex
File: pgfcoreobjects.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.code.te
x
File: pgfcorepathprocessing.code.tex 2023-01-15 v3.1.10 (3.1.10)
) (/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex
File: pgfcorearrows.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfarrowsep=\dimen197
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex
File: pgfcoreshade.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@max=\dimen198
\pgf@sys@shading@range@num=\count292
\pgf@shadingcount=\count293
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex
File: pgfcoreimage.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex
File: pgfcoreexternal.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfexternal@startupbox=\box57
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex
File: pgfcorelayers.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code.tex
File: pgfcoretransparency.code.tex 2023-01-15 v3.1.10 (3.1.10)
) (/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex
File: pgfcorepatterns.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex
File: pgfcorerdf.code.tex 2023-01-15 v3.1.10 (3.1.10)
)))
(/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.tex
File: pgfmoduleshapes.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfnodeparttextbox=\box58
)
(/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex
File: pgfmoduleplot.code.tex 2023-01-15 v3.1.10 (3.1.10)
)
(/usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
Package: pgfcomp-version-0-65 2023-01-15 v3.1.10 (3.1.10)
\pgf@nodesepstart=\dimen199
\pgf@nodesepend=\dimen256
)
(/usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
Package: pgfcomp-version-1-18 2023-01-15 v3.1.10 (3.1.10)
))
(/usr/share/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
(/usr/share/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex))
(/usr/share/texmf-dist/tex/latex/pgf/math/pgfmath.sty
(/usr/share/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex))
(/usr/share/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
Package: pgffor 2023-01-15 v3.1.10 (3.1.10)
\pgffor@iter=\dimen257
\pgffor@skip=\dimen258
\pgffor@stack=\toks32
\pgffor@toks=\toks33
))
(/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
Package: tikz 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.te
x
File: pgflibraryplothandlers.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgf@plot@mark@count=\count294
\pgfplotmarksize=\dimen259
)
\tikz@lastx=\dimen260
\tikz@lasty=\dimen261
\tikz@lastxsaved=\dimen262
\tikz@lastysaved=\dimen263
\tikz@lastmovetox=\dimen264
\tikz@lastmovetoy=\dimen265
\tikzleveldistance=\dimen266
\tikzsiblingdistance=\dimen267
\tikz@figbox=\box59
\tikz@figbox@bg=\box60
\tikz@tempbox=\box61
\tikz@tempbox@bg=\box62
\tikztreelevel=\count295
\tikznumberofchildren=\count296
\tikznumberofcurrentchild=\count297
\tikz@fig@count=\count298
(/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex
File: pgfmodulematrix.code.tex 2023-01-15 v3.1.10 (3.1.10)
\pgfmatrixcurrentrow=\count299
\pgfmatrixcurrentcolumn=\count300
\pgf@matrix@numberofcolumns=\count301
)
\tikz@expandcount=\count302
(/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrary
topaths.code.tex
File: tikzlibrarytopaths.code.tex 2023-01-15 v3.1.10 (3.1.10)
))) (/usr/share/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2024/11/05 v2.17t AMS math features
\@mathmargin=\skip51
For additional information on amsmath, use the `?' option.
(/usr/share/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/share/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks34
\ex@=\dimen268
))
(/usr/share/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen269
)
(/usr/share/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2022/04/08 v2.04 operator names
)
\inf@bad=\count303
LaTeX Info: Redefining \frac on input line 233.
\uproot@=\count304
\leftroot@=\count305
LaTeX Info: Redefining \overline on input line 398.
LaTeX Info: Redefining \colon on input line 409.
\classnum@=\count306
\DOTSCASE@=\count307
LaTeX Info: Redefining \ldots on input line 495.
LaTeX Info: Redefining \dots on input line 498.
LaTeX Info: Redefining \cdots on input line 619.
\Mathstrutbox@=\box63
\strutbox@=\box64
LaTeX Info: Redefining \big on input line 721.
LaTeX Info: Redefining \Big on input line 722.
LaTeX Info: Redefining \bigg on input line 723.
LaTeX Info: Redefining \Bigg on input line 724.
\big@size=\dimen270
LaTeX Font Info: Redeclaring font encoding OML on input line 742.
LaTeX Font Info: Redeclaring font encoding OMS on input line 743.
\macc@depth=\count308
LaTeX Info: Redefining \bmod on input line 904.
LaTeX Info: Redefining \pmod on input line 909.
LaTeX Info: Redefining \smash on input line 939.
LaTeX Info: Redefining \relbar on input line 969.
LaTeX Info: Redefining \Relbar on input line 970.
\c@MaxMatrixCols=\count309
\dotsspace@=\muskip17
\c@parentequation=\count310
\dspbrk@lvl=\count311
\tag@help=\toks35
\row@=\count312
\column@=\count313
\maxfields@=\count314
\andhelp@=\toks36
\eqnshift@=\dimen271
\alignsep@=\dimen272
\tagshift@=\dimen273
\tagwidth@=\dimen274
\totwidth@=\dimen275
\lineht@=\dimen276
\@envbody=\toks37
\multlinegap=\skip52
\multlinetaggap=\skip53
\mathdisplay@stack=\toks38
LaTeX Info: Redefining \[ on input line 2953.
LaTeX Info: Redefining \] on input line 2954.
)
(/usr/share/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
(/usr/share/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
))
(/usr/share/texmf-dist/tex/latex/enumitem/enumitem.sty
Package: enumitem 2025/02/06 v3.11 Customized lists
\labelindent=\skip54
\enit@outerparindent=\dimen277
\enit@toks=\toks39
\enit@inbox=\box65
\enit@count@id=\count315
\enitdp@description=\count316
)
(/usr/share/texmf-dist/tex/latex/tcolorbox/tcolorbox.sty
Package: tcolorbox 2024/10/22 version 6.4.1 text color boxes
(/usr/share/texmf-dist/tex/latex/tools/verbatim.sty
Package: verbatim 2024-01-22 v1.5x LaTeX2e package for verbatim enhancements
\every@verbatim=\toks40
\verbatim@line=\toks41
\verbatim@in@stream=\read4
)
(/usr/share/texmf-dist/tex/latex/environ/environ.sty
Package: environ 2014/05/04 v0.3 A new way to define environments
(/usr/share/texmf-dist/tex/latex/trimspaces/trimspaces.sty
Package: trimspaces 2009/09/17 v1.1 Trim spaces around a token list
))
(/usr/share/texmf-dist/tex/latex/etoolbox/etoolbox.sty
Package: etoolbox 2025/02/11 v2.5l e-TeX tools for LaTeX (JAW)
\etb@tempcnta=\count317
)
\tcb@titlebox=\box66
\tcb@upperbox=\box67
\tcb@lowerbox=\box68
\tcb@phantombox=\box69
\c@tcbbreakpart=\count318
\c@tcblayer=\count319
\c@tcolorbox@number=\count320
\l__tcobox_tmpa_box=\box70
\l__tcobox_tmpa_dim=\dimen278
\tcb@temp=\box71
\tcb@temp=\box72
\tcb@temp=\box73
\tcb@temp=\box74
)
(/usr/share/texmf-dist/tex/latex/fancyhdr/fancyhdr.sty
Package: fancyhdr 2025/02/07 v5.2 Extensive control of page headers and footers
\f@nch@headwidth=\skip55
\f@nch@offset@elh=\skip56
\f@nch@offset@erh=\skip57
\f@nch@offset@olh=\skip58
\f@nch@offset@orh=\skip59
\f@nch@offset@elf=\skip60
\f@nch@offset@erf=\skip61
\f@nch@offset@olf=\skip62
\f@nch@offset@orf=\skip63
\f@nch@height=\skip64
\f@nch@footalignment=\skip65
\f@nch@widthL=\skip66
\f@nch@widthC=\skip67
\f@nch@widthR=\skip68
\@temptokenb=\toks42
)
(/usr/share/texmf-dist/tex/latex/tools/multicol.sty
Package: multicol 2024/09/14 v1.9i multicolumn formatting (FMi)
\c@tracingmulticols=\count321
\mult@box=\box75
\multicol@leftmargin=\dimen279
\c@unbalance=\count322
\c@collectmore=\count323
\doublecol@number=\count324
\multicoltolerance=\count325
\multicolpretolerance=\count326
\full@width=\dimen280
\page@free=\dimen281
\premulticols=\dimen282
\postmulticols=\dimen283
\multicolsep=\skip69
\multicolbaselineskip=\skip70
\partial@page=\box76
\last@line=\box77
\mc@boxedresult=\box78
\maxbalancingoverflow=\dimen284
\mult@rightbox=\box79
\mult@grightbox=\box80
\mult@firstbox=\box81
\mult@gfirstbox=\box82
\@tempa=\box83
\@tempa=\box84
\@tempa=\box85
\@tempa=\box86
\@tempa=\box87
\@tempa=\box88
\@tempa=\box89
\@tempa=\box90
\@tempa=\box91
\@tempa=\box92
\@tempa=\box93
\@tempa=\box94
\@tempa=\box95
\@tempa=\box96
\@tempa=\box97
\@tempa=\box98
\@tempa=\box99
\@tempa=\box100
\@tempa=\box101
\@tempa=\box102
\@tempa=\box103
\@tempa=\box104
\@tempa=\box105
\@tempa=\box106
\@tempa=\box107
\@tempa=\box108
\@tempa=\box109
\@tempa=\box110
\@tempa=\box111
\@tempa=\box112
\@tempa=\box113
\@tempa=\box114
\@tempa=\box115
\@tempa=\box116
\@tempa=\box117
\@tempa=\box118
\c@minrows=\count327
\c@columnbadness=\count328
\c@finalcolumnbadness=\count329
\last@try=\dimen285
\multicolovershoot=\dimen286
\multicolundershoot=\dimen287
\mult@nat@firstbox=\box119
\colbreak@box=\box120
\mc@col@check@num=\count330
)
(/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrary
shapes.geometric.code.tex
File: tikzlibraryshapes.geometric.code.tex 2023-01-15 v3.1.10 (3.1.10)
(/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.geomet
ric.code.tex
File: pgflibraryshapes.geometric.code.tex 2023-01-15 v3.1.10 (3.1.10)
))
(/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrary
calc.code.tex
File: tikzlibrarycalc.code.tex 2023-01-15 v3.1.10 (3.1.10)
) (/usr/share/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2024-05-08 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count331
\l__pdf_internal_box=\box121
) (./activite1.aux
(/usr/share/texmf-dist/tex/generic/babel/locale/fr/babel-french.tex
Package babel Info: Importing font and identification data for french
(babel) from babel-fr.ini. Reported on input line 11.
))
\openout1 = `activite1.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 33.
LaTeX Font Info: ... okay on input line 33.
LaTeX Info: Redefining \degres on input line 33.
Package french.ldf Info: Setting StandardItemizeEnv=true for
(french.ldf) compatibility with enumitem package,
(french.ldf) reported on input line 33.
Package french.ldf Info: Setting StandardEnumerateEnv=true for
(french.ldf) compatibility with enumitem package,
(french.ldf) reported on input line 33.
LaTeX Info: Redefining \up on input line 33.
*geometry* driver: auto-detecting
*geometry* detected driver: pdftex
*geometry* verbose mode - [ preamble ] result:
* driver: pdftex
* paper: a4paper
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(56.9055pt, 483.69687pt, 56.9055pt)
* v-part:(T,H,B)=(56.9055pt, 731.23584pt, 56.9055pt)
* \paperwidth=597.50787pt
* \paperheight=845.04684pt
* \textwidth=483.69687pt
* \textheight=731.23584pt
* \oddsidemargin=-15.36449pt
* \evensidemargin=-15.36449pt
* \topmargin=-52.36449pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=11.0pt
* \footskip=30.0pt
* \marginparwidth=50.0pt
* \marginparsep=10.0pt
* \columnsep=10.0pt
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
(/usr/share/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count332
\scratchdimen=\dimen288
\scratchbox=\box122
\nofMPsegments=\count333
\nofMParguments=\count334
\everyMPshowfont=\toks43
\MPscratchCnt=\count335
\MPscratchDim=\dimen289
\MPnumerator=\count336
\makeMPintoPDFobject=\count337
\everyMPtoPDFconversion=\toks44
) (/usr/share/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/share/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
LaTeX Font Info: Trying to load font information for U+msa on input line 59.
(/usr/share/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 59.
(/usr/share/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
)
[1
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
/enc/dvips/cm-super/cm-super-t1.enc}]
[2]
[3]
[4] (./activite1.aux)
***********
LaTeX2e <2024-11-01> patch level 2
L3 programming layer <2025-01-18>
***********
)
Here is how much of TeX's memory you used:
19631 strings out of 475142
396084 string characters out of 5765947
784639 words of memory out of 5000000
42341 multiletter control sequences out of 15000+600000
571118 words of font info for 64 fonts, out of 8000000 for 9000
14 hyphenation exceptions out of 8191
102i,11n,107p,410b,737s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/share/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/share/
texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/share/texmf-dist/font
s/type1/public/amsfonts/cm/cmsy10.pfb></usr/share/texmf-dist/fonts/type1/public
/amsfonts/symbols/msam10.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super
/sfbx1095.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb><
/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1440.pfb></usr/share/texm
f-dist/fonts/type1/public/cm-super/sfbx2488.pfb></usr/share/texmf-dist/fonts/ty
pe1/public/cm-super/sfrm1000.pfb></usr/share/texmf-dist/fonts/type1/public/cm-s
uper/sfrm1095.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfti1095.p
fb>
Output written on activite1.pdf (4 pages, 182024 bytes).
PDF statistics:
76 PDF objects out of 1000 (max. 8388607)
47 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
13 words of extra memory for PDF output out of 10000 (max. 10000000)

BIN
activite1/activite1.pdf Normal file

Binary file not shown.

Binary file not shown.

301
activite1/activite1.tex Normal file
View file

@ -0,0 +1,301 @@
\documentclass[a4paper,11pt]{article}
% Packages
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[francais]{babel}
\usepackage{geometry}
\usepackage{tikz}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{enumitem}
\usepackage{xcolor}
\usepackage{tcolorbox}
\usepackage{fancyhdr}
\usepackage{multicol}
% Configuration de la page
\geometry{margin=2cm}
\pagestyle{fancy}
\fancyhf{}
\fancyfoot[C]{Page \thepage/4 -- Activité k-NN}
\renewcommand{\headrulewidth}{0pt}
% Couleurs personnalisées
\definecolor{classA}{RGB}{231,76,60}
\definecolor{classB}{RGB}{52,152,219}
\definecolor{lightblue}{RGB}{227,242,253}
\definecolor{lightyellow}{RGB}{255,243,205}
% Configuration TikZ
\usetikzlibrary{shapes.geometric,calc}
\begin{document}
% ============= PAGE 1 =============
\begin{center}
{\Huge \textbf{Activité débranchée : k-NN}}\\[0.3cm]
{\Large \textbf{Les k Plus Proches Voisins}}
\end{center}
\vspace{0.5cm}
\textbf{Objectif :} Comprendre comment l'algorithme k-NN classifie un nouveau point en utilisant la proximité avec les points d'apprentissage.
\vspace{0.3cm}
\begin{tcolorbox}[colback=lightblue,colframe=blue!75!black,title=\textbf{Principe}]
\begin{itemize}[leftmargin=*]
\item On dispose d'un ensemble de points déjà classifiés (points d'apprentissage)
\item Pour classifier un nouveau point, on cherche ses \textit{k} plus proches voisins
\item La classe majoritaire parmi ces \textit{k} voisins devient la classe du nouveau point
\end{itemize}
\end{tcolorbox}
\vspace{0.5cm}
\section*{Exercice 1 : Classification avec k = 3}
Voici un ensemble de points sur un plan. Les cercles rouges (\textcolor{classA}{$\bullet$}) sont de classe A, les carrés bleus (\textcolor{classB}{$\blacksquare$}) sont de classe B. Le point noir ($\star$) est à classifier.
\vspace{0.3cm}
\begin{center}
\begin{tikzpicture}[scale=1.2]
% Grille
\draw[gray!30, step=1] (0,0) grid (8,8);
% Axes et cadre
\draw[thick] (0,0) rectangle (8,8);
% Points de classe A (cercles rouges)
\foreach \point in {(1,7), (1.5,6), (2,7.5), (2.5,6.5), (1.5,5), (2.5,5.5), (3,7)} {
\fill[classA] \point circle (0.15);
}
% Points de classe B (carrés bleus)
\fill[classB] (5,2) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]5,2) rectangle ++(0.24,0.24);
\fill[classB] (5.5,3) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]5.5,3) rectangle ++(0.24,0.24);
\fill[classB] (6,1.5) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]6,1.5) rectangle ++(0.24,0.24);
\fill[classB] (6.5,2.5) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]6.5,2.5) rectangle ++(0.24,0.24);
\fill[classB] (5.5,1) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]5.5,1) rectangle ++(0.24,0.24);
\fill[classB] (7,2) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]7,2) rectangle ++(0.24,0.24);
\fill[classB] (6.5,3.5) circle (0.15);
\fill[classB] ([shift={(-0.12,-0.12)}]6.5,3.5) rectangle ++(0.24,0.24);
% Point à classifier (étoile noire)
\node[star,star points=5,star point ratio=2.5,fill=black,draw=black,thick,minimum size=0.6cm] at (4,4.5) {};
\end{tikzpicture}
\vspace{0.3cm}
% Légende
\begin{tabular}{ccccc}
\textcolor{classA}{$\bullet$} Classe A & \quad &
\textcolor{classB}{$\blacksquare$} Classe B & \quad &
$\star$ À classifier
\end{tabular}
\end{center}
\vspace{0.5cm}
\subsection*{Questions :}
\begin{enumerate}
\item Identifiez les 3 points les plus proches du point noir $\star$. Tracez les distances sur le graphique ci-dessus.
\vspace{0.3cm}
Distances : \underline{\hspace{3cm}} \underline{\hspace{3cm}} \underline{\hspace{3cm}}
\vspace{0.3cm}
\item Parmi ces 3 points, combien sont de classe A ? \underline{\hspace{1.5cm}} De classe B ? \underline{\hspace{1.5cm}}
\vspace{0.3cm}
\item À quelle classe appartient donc le point noir avec $k = 3$ ? \underline{\hspace{3cm}}
\end{enumerate}
% ============= PAGE 2 =============
%\newpage
\section*{Exercice 2 : Influence du paramètre k}
Reprenez le même graphique que l'exercice 1.
\vspace{0.5cm}
\subsection*{a) Avec k = 1 (1 seul voisin)}
\begin{itemize}
\item Quel est le point le plus proche ? \underline{\hspace{5cm}}
\item Classification du point noir : \underline{\hspace{5cm}}
\end{itemize}
\vspace{0.5cm}
\subsection*{b) Avec k = 5 (5 voisins)}
\begin{itemize}
\item Listez les 5 points les plus proches : \underline{\hspace{7cm}}
\vspace{0.2cm}
\underline{\hspace{10cm}}
\item Nombre de classe A : \underline{\hspace{2cm}} \quad Nombre de classe B : \underline{\hspace{2cm}}
\item Classification du point noir : \underline{\hspace{5cm}}
\end{itemize}
\vspace{0.5cm}
\subsection*{c) Avec k = 7 (tous les voisins de chaque classe)}
\begin{itemize}
\item Nombre de classe A : \underline{\hspace{2cm}} \quad Nombre de classe B : \underline{\hspace{2cm}}
\item Classification du point noir : \underline{\hspace{5cm}}
\end{itemize}
\vspace{0.8cm}
\begin{tcolorbox}[colback=lightyellow,colframe=orange!75!black,title=\textbf{Réflexion}]
\textbf{1. La classification change-t-elle selon la valeur de k ? Pourquoi ?}
\vspace{0.8cm}
\underline{\hspace{14cm}}
\vspace{0.4cm}
\underline{\hspace{14cm}}
\vspace{0.5cm}
\textbf{2. Que se passe-t-il si k est trop petit (k = 1) ?}
\vspace{0.8cm}
\underline{\hspace{14cm}}
\vspace{0.4cm}
\underline{\hspace{14cm}}
\vspace{0.5cm}
\textbf{3. Que se passe-t-il si k est trop grand (k = nombre total de points) ?}
\vspace{0.8cm}
\underline{\hspace{14cm}}
\vspace{0.4cm}
\underline{\hspace{14cm}}
\end{tcolorbox}
% ============= PAGE 3 =============
\newpage
\section*{Exercice 3 : À vous de jouer !}
Créez votre propre situation de classification avec $k = 3$.
\vspace{0.5cm}
\begin{center}
\begin{tikzpicture}[scale=1.3]
% Grille fine
\draw[gray!20, step=1] (0,0) grid (12,12);
% Axes et cadre
\draw[very thick] (0,0) rectangle (12,12);
% Graduations
\foreach \x in {0,2,4,6,8,10,12} {
\node[below] at (\x,-0.2) {\small \x};
}
\foreach \y in {0,2,4,6,8,10,12} {
\node[left] at (-0.2,\y) {\small \y};
}
\end{tikzpicture}
\end{center}
\vspace{0.5cm}
\begin{tcolorbox}[colback=lightblue,colframe=blue!75!black,title=\textbf{Instructions}]
\begin{itemize}
\item Placez au moins 10 points : 5 cercles rouges (classe A) et 5 carrés bleus (classe B)
\item Placez une étoile noire (point à classifier)
\item Tracez les 3 distances les plus courtes
\item Déterminez la classe du point noir
\end{itemize}
\end{tcolorbox}
\vspace{0.5cm}
\textbf{Classification finale :} \underline{\hspace{5cm}}
% ============= PAGE 4 =============
\newpage
\section*{Pour aller plus loin}
\subsection*{Applications réelles de k-NN}
\begin{itemize}
\item Reconnaissance d'écriture manuscrite
\item Systèmes de recommandation (films, musique, produits)
\item Diagnostic médical (classification de maladies)
\item Détection de spam dans les emails
\item Reconnaissance faciale
\item Prévision météorologique
\end{itemize}
\vspace{0.5cm}
\subsection*{Questions de réflexion}
\textbf{1. Avantages de k-NN :}
\begin{itemize}
\item Simple à comprendre et à implémenter
\item Pas besoin d'entraînement complexe
\item Fonctionne bien pour des frontières non linéaires
\end{itemize}
\vspace{0.3cm}
\textbf{2. Limitations de k-NN :}
\begin{itemize}
\item Lent avec beaucoup de données (doit calculer toutes les distances)
\item Sensible aux données aberrantes
\item Nécessite de choisir la bonne valeur de $k$
\item Ne fonctionne pas bien si les classes sont déséquilibrées
\end{itemize}
\vspace{0.3cm}
\textbf{3. Comment choisir k ?}
\begin{itemize}
\item $k$ trop petit ($k=1$) : sensible au bruit, risque de sur-apprentissage
\item $k$ trop grand : perd les détails, risque de sous-apprentissage
\item Conseil : tester plusieurs valeurs et choisir celle qui donne les meilleurs résultats
\item Souvent, $k$ impair pour éviter les égalités ($k = 3, 5, 7...$)
\end{itemize}
\vspace{0.5cm}
\begin{tcolorbox}[colback=lightyellow,colframe=orange!75!black,title=\textbf{Défi bonus}]
Imaginez une situation où k-NN pourrait donner un mauvais résultat. Dessinez cette situation sur une feuille séparée et expliquez pourquoi.
\vspace{0.5cm}
\textit{Indices : Pensez aux données bruitées, aux classes déséquilibrées, ou aux points isolés...}
\end{tcolorbox}
\end{document}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,693 @@
"""
Script pour générer une page HTML interactive avec les données des villages corses embarquées.
Usage: python generate_interactive_map.py villages_corse.csv
"""
import csv
import json
import sys
def parse_coordinates(point_geo_str):
"""Parse la colonne Point_Geo"""
try:
parts = point_geo_str.split(',')
lat = float(parts[0].strip())
lon = float(parts[1].strip())
return lat, lon
except:
return None, None
def load_villages_from_csv(csv_file):
"""Charge les villages depuis le CSV"""
villages = []
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter=';')
next(reader) # Skip header
for row in reader:
if len(row) >= 18:
lat, lon = parse_coordinates(row[17])
if lat and lon:
villages.append({
'name': row[0],
'nameCorse': row[1],
'lat': lat,
'lon': lon,
'dept': row[9],
'altitude': float(row[15]) if row[15] else 0
})
return villages
def generate_html(villages, output_file='knn_interactive_full.html'):
"""Génère le fichier HTML avec les données embarquées"""
villages_json = json.dumps(villages, ensure_ascii=False)
html_content = f'''<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Classification k-NN Interactive - Corse</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.header p {{
font-size: 1.2em;
opacity: 0.9;
}}
.main-content {{
display: grid;
grid-template-columns: 350px 1fr;
gap: 0;
}}
.sidebar {{
background: #f5f5f5;
padding: 30px;
border-right: 2px solid #ddd;
max-height: 900px;
overflow-y: auto;
}}
.controls {{
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.controls h3 {{
margin-bottom: 15px;
color: #333;
font-size: 1.2em;
}}
.control-group {{
margin-bottom: 20px;
}}
.control-group label {{
display: block;
font-weight: bold;
color: #555;
margin-bottom: 8px;
}}
.slider-container {{
display: flex;
align-items: center;
gap: 10px;
}}
input[type="range"] {{
flex: 1;
height: 8px;
border-radius: 5px;
background: #ddd;
outline: none;
}}
input[type="range"]::-webkit-slider-thumb {{
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}}
input[type="range"]::-moz-range-thumb {{
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
}}
.k-value {{
background: #667eea;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
min-width: 40px;
text-align: center;
}}
.result-box {{
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.result-box h3 {{
margin-bottom: 15px;
color: #333;
}}
.prediction {{
font-size: 1.3em;
font-weight: bold;
padding: 15px;
border-radius: 8px;
text-align: center;
margin-bottom: 15px;
}}
.prediction.corse-sud {{
background: #ffebee;
color: #c62828;
}}
.prediction.haute-corse {{
background: #e3f2fd;
color: #1565c0;
}}
.prediction.no-result {{
background: #f5f5f5;
color: #666;
}}
.coords {{
font-size: 0.9em;
color: #666;
text-align: center;
margin-bottom: 10px;
}}
.votes {{
display: flex;
justify-content: space-around;
margin-bottom: 15px;
}}
.vote-item {{
text-align: center;
}}
.vote-count {{
font-size: 2em;
font-weight: bold;
}}
.vote-count.red {{
color: #c62828;
}}
.vote-count.blue {{
color: #1565c0;
}}
.neighbors-list {{
max-height: 300px;
overflow-y: auto;
background: #fafafa;
padding: 10px;
border-radius: 5px;
}}
.neighbor-item {{
padding: 10px;
margin-bottom: 8px;
background: white;
border-radius: 5px;
border-left: 4px solid #ddd;
}}
.neighbor-item.dept-2a {{
border-left-color: #c62828;
}}
.neighbor-item.dept-2b {{
border-left-color: #1565c0;
}}
.neighbor-name {{
font-weight: bold;
margin-bottom: 3px;
}}
.neighbor-corse {{
font-style: italic;
color: #666;
font-size: 0.9em;
}}
.neighbor-distance {{
font-size: 0.9em;
color: #666;
}}
.instructions {{
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}}
.instructions h4 {{
margin-bottom: 10px;
color: #856404;
}}
.instructions ul {{
margin-left: 20px;
color: #856404;
}}
.instructions li {{
margin-bottom: 5px;
}}
.legend {{
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.legend h4 {{
margin-bottom: 10px;
color: #333;
}}
.legend-item {{
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}}
.legend-circle {{
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #333;
}}
.legend-circle.red {{
background: #e74c3c;
}}
.legend-circle.blue {{
background: #3498db;
}}
.legend-circle.marker {{
background: #95a5a6;
border: 3px solid #000;
}}
#map {{
height: 900px;
cursor: crosshair;
}}
.leaflet-popup-content {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
button {{
padding: 10px 20px;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin-bottom: 10px;
}}
button:hover {{
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}}
.btn-reset {{
background: #f44336;
color: white;
}}
.btn-random {{
background: #4CAF50;
color: white;
}}
.stats {{
background: #e3f2fd;
padding: 10px;
border-radius: 5px;
font-size: 0.9em;
text-align: center;
margin-top: 15px;
color: #1565c0;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺 Classification k-NN Interactive</h1>
<p>Haute-Corse ou Corse du Sud ?</p>
<div class="stats">
<strong>{len(villages)}</strong> villages chargés
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="instructions">
<h4>📋 Instructions</h4>
<ul>
<li><strong>Cliquez</strong> n'importe où sur la carte</li>
<li>Ajustez la valeur de <strong>k</strong> avec le curseur</li>
<li>Observez les <strong>k plus proches villages</strong></li>
<li>La <strong>classification</strong> se fait par vote majoritaire</li>
</ul>
</div>
<div class="controls">
<h3> Paramètres</h3>
<div class="control-group">
<label>Nombre de voisins (k) :</label>
<div class="slider-container">
<input type="range" id="kSlider" min="1" max="21" value="5" step="2">
<span class="k-value" id="kValue">5</span>
</div>
</div>
<button class="btn-random" onclick="placeRandomPoint()">🎲 Point Aléatoire</button>
<button class="btn-reset" onclick="resetClassification()">🔄 Réinitialiser</button>
</div>
<div class="result-box" id="resultBox">
<h3>🎯 Résultat</h3>
<div class="prediction no-result" id="prediction">
Cliquez sur la carte
</div>
<div class="coords" id="coords"></div>
<div id="votesContainer" style="display: none;">
<div class="votes">
<div class="vote-item">
<div class="vote-count red" id="votes2A">0</div>
<div>Corse du Sud</div>
</div>
<div class="vote-item">
<div class="vote-count blue" id="votes2B">0</div>
<div>Haute-Corse</div>
</div>
</div>
<h4>🏘 Plus proches voisins :</h4>
<div class="neighbors-list" id="neighborsList"></div>
</div>
</div>
<div class="legend">
<h4>Légende</h4>
<div class="legend-item">
<div class="legend-circle red"></div>
<span>Corse du Sud (2A)</span>
</div>
<div class="legend-item">
<div class="legend-circle blue"></div>
<span>Haute-Corse (2B)</span>
</div>
<div class="legend-item">
<div class="legend-circle marker"></div>
<span>Point à classifier</span>
</div>
</div>
</div>
<div id="map"></div>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Données des villages (embarquées depuis le CSV)
const villages = {villages_json};
let map, testMarker, neighborMarkers = [], neighborLines = [];
let currentK = 5;
// Initialiser la carte
function initMap() {{
map = L.map('map').setView([42.15, 9.05], 9);
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '© OpenStreetMap contributors'
}}).addTo(map);
// Ajouter tous les villages
villages.forEach(village => {{
const color = village.dept === '2A' ? '#e74c3c' : '#3498db';
L.circleMarker([village.lat, village.lon], {{
radius: 3,
fillColor: color,
color: '#333',
weight: 1,
opacity: 1,
fillOpacity: 0.5
}}).bindPopup(`<b>${{village.name}}</b><br>${{village.nameCorse}}<br>${{village.dept}}`)
.addTo(map);
}});
// Ajouter le gestionnaire de clic
map.on('click', onMapClick);
}}
// Calcul de distance Haversine
function haversineDistance(lat1, lon1, lat2, lon2) {{
const R = 6371; // Rayon de la Terre en km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}}
// Classification k-NN
function knnClassify(lat, lon, k) {{
// Calculer distances
const distances = villages.map(village => ({{
...village,
distance: haversineDistance(lat, lon, village.lat, village.lon)
}}));
// Trier par distance
distances.sort((a, b) => a.distance - b.distance);
// Prendre les k plus proches
const neighbors = distances.slice(0, k);
// Voter
const votes = {{ '2A': 0, '2B': 0 }};
neighbors.forEach(n => votes[n.dept]++);
const prediction = votes['2A'] > votes['2B'] ? '2A' : '2B';
return {{ prediction, neighbors, votes }};
}}
// Gestionnaire de clic sur la carte
function onMapClick(e) {{
const lat = e.latlng.lat;
const lon = e.latlng.lng;
classifyPoint(lat, lon);
}}
// Classifier un point
function classifyPoint(lat, lon) {{
// Supprimer les anciens marqueurs
if (testMarker) map.removeLayer(testMarker);
neighborMarkers.forEach(m => map.removeLayer(m));
neighborLines.forEach(l => map.removeLayer(l));
neighborMarkers = [];
neighborLines = [];
// Classifier
const result = knnClassify(lat, lon, currentK);
// Ajouter le marqueur du point test
testMarker = L.circleMarker([lat, lon], {{
radius: 12,
fillColor: '#95a5a6',
color: '#000',
weight: 3,
opacity: 1,
fillOpacity: 0.8
}}).addTo(map);
// Ajouter les marqueurs et lignes des voisins
result.neighbors.forEach(neighbor => {{
const nColor = neighbor.dept === '2A' ? '#e74c3c' : '#3498db';
// Ligne
const line = L.polyline(
[[lat, lon], [neighbor.lat, neighbor.lon]],
{{color: nColor, weight: 2, opacity: 0.5}}
).addTo(map);
neighborLines.push(line);
// Marqueur
const marker = L.circleMarker([neighbor.lat, neighbor.lon], {{
radius: 8,
fillColor: nColor,
color: '#333',
weight: 2,
opacity: 1,
fillOpacity: 0.8
}}).bindPopup(`<b>${{neighbor.name}}</b><br>${{neighbor.nameCorse}}<br>${{neighbor.dept}}<br>Distance: ${{neighbor.distance.toFixed(2)}} km`)
.addTo(map);
neighborMarkers.push(marker);
}});
// Afficher les résultats
displayResults(lat, lon, result);
}}
// Afficher les résultats
function displayResults(lat, lon, result) {{
const predictionDiv = document.getElementById('prediction');
const coordsDiv = document.getElementById('coords');
const votesContainer = document.getElementById('votesContainer');
const votes2A = document.getElementById('votes2A');
const votes2B = document.getElementById('votes2B');
const neighborsList = document.getElementById('neighborsList');
// Coordonnées
coordsDiv.textContent = `(${{lat.toFixed(4)}}, ${{lon.toFixed(4)}})`;
// Prédiction
const deptName = result.prediction === '2A' ? 'Corse du Sud (2A)' : 'Haute-Corse (2B)';
const cssClass = result.prediction === '2A' ? 'corse-sud' : 'haute-corse';
predictionDiv.textContent = deptName;
predictionDiv.className = 'prediction ' + cssClass;
// Votes
votes2A.textContent = result.votes['2A'];
votes2B.textContent = result.votes['2B'];
votesContainer.style.display = 'block';
// Liste des voisins
neighborsList.innerHTML = '';
result.neighbors.forEach(neighbor => {{
const div = document.createElement('div');
div.className = `neighbor-item dept-${{neighbor.dept.toLowerCase()}}`;
div.innerHTML = `
<div class="neighbor-name">${{neighbor.name}} (${{neighbor.dept}})</div>
<div class="neighbor-corse">${{neighbor.nameCorse}}</div>
<div class="neighbor-distance">Distance: ${{neighbor.distance.toFixed(2)}} km</div>
`;
neighborsList.appendChild(div);
}});
}}
// Point aléatoire
function placeRandomPoint() {{
const lat = 41.3 + Math.random() * (43.0 - 41.3);
const lon = 8.5 + Math.random() * (9.6 - 8.5);
classifyPoint(lat, lon);
map.setView([lat, lon], 10);
}}
// Réinitialiser
function resetClassification() {{
if (testMarker) map.removeLayer(testMarker);
neighborMarkers.forEach(m => map.removeLayer(m));
neighborLines.forEach(l => map.removeLayer(l));
neighborMarkers = [];
neighborLines = [];
testMarker = null;
document.getElementById('prediction').textContent = 'Cliquez sur la carte';
document.getElementById('prediction').className = 'prediction no-result';
document.getElementById('coords').textContent = '';
document.getElementById('votesContainer').style.display = 'none';
map.setView([42.15, 9.05], 9);
}}
// Gestionnaire du slider k
document.getElementById('kSlider').addEventListener('input', function(e) {{
currentK = parseInt(e.target.value);
document.getElementById('kValue').textContent = currentK;
// Reclassifier si un point existe
if (testMarker) {{
const latlng = testMarker.getLatLng();
classifyPoint(latlng.lat, latlng.lng);
}}
}});
// Initialisation
initMap();
</script>
</body>
</html>'''
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ Fichier HTML généré : {output_file}")
print(f"📊 {len(villages)} villages inclus")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python generate_interactive_map.py villages_corse.csv")
sys.exit(1)
csv_file = sys.argv[1]
villages = load_villages_from_csv(csv_file)
generate_html(villages)

View file

@ -0,0 +1,137 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (anonymous) /CreationDate (D:20251023100834+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20251023100834+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 4 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2409
>>
stream
Gat%$CQI5[(&dQ-EPH@r8@a,3>+d9%qe-??]@1rM:BM;gX<^DSfp1pgU;+EtPXs3qfM45Ya)c3FmZX!('ZBMdib,SDHM^7jr?=d'HMg#p1JNr!ki]ud`$ItrE*8iC-8:b=8lgP62b[2ZU;nk!qOookce[PO)+!TXq0o:>k#;'+deECq&&8,?RN=]Bj0s^TTqcYOr%0d,1*\7be\#ZfqEb^^j%']uY5*`8s1.I8Xrl!CiL5ShMTK)hdPJekjMZ!*5I^.B!t>0K\(qR3^E-QnG2$lg+)(7?^%o/W]#1F_)]bYb;ibG_TkioF;nkCX\+PmW]<4Ag>'$ZC#c9+&U=QcNj.Y4L@i^f`ZlaJ+GM>-DolJm7#&ZoaOk/F>qdA'M)oqebbO-da'f,*g%#SqZ3<q:AU-0#Sf/MM,BmZ9'ETXAlm]>MD]20_Sod$'&hg/IMj*iqq0GH6Uqm"tkhr([r3tU3Dr1l0qi=!^t:Tn)Jn`f^01ULWFC:fl'YWE=fEe]M=]V/V,.%UJ>BoF@G%jeWU9Bu>LP.K-n/^q_N7IRj7db<-XrZ!DamSH.FHJ!Dj6o?bVipkg3;nD-,lf5)SMRu<rH8a"(W+T$H-CpQVnal3gZ"mMKTVCUo2"dPpqm!B>clYimY]Jgd(Us*tEJA2(V@(<YCcZl&hr?H2J%qt07Uc0PYUL:YYWI;M6e\=7ms\+]C0tgipsba6:TnKf+`L73#O[@+GP6QlJk8o:BPPoLeAEuj*)&M%2kg7T'A`r>-AR`%DRdjaQ`H)4GbC6pd159pk0^0Cs*5pS5HCC&H-rs_,K25J=u_Zn7d%oIW4[V]R$URCNL^"Q'/PbCTKt&Gr'Ies0V]noK[R4.=/f6FD.Q?IXj!QjAnVe\'6?dG'AA+FV+%HZ$9Pek:!cRlh8c(1-aX2l8m;TK!a.gYTUE7Z9M6U+BG5QK<f;hQkt5_I1l:@"'XSai8uj!(:7&%<16WUr]<M6e1gsHWSRk]>;mI``VY,!H/SPlANRi'nN5Z(,ct4\^MPY.,Qf6ssL8%lYiL!Wj(Bc$rTaZJgk]KYb_!/]&X.h9]dU8Vr29ln6]OF4d9OF/&ACcNY$RV8UhGV2.R1cAr2ZZ3K[OK=/ZSTk:F3X";\%uj&(AVeJ@19EtUE`a>i^btfj6:LtdP57Ya"d]H,eCYH^6F5Kgmp-1A6'MtAPAen,U/G?S7%WTVU1ZkYV'(DV6]':khN`0Zo5#V)Ok&XjtrF@l\nD'g]omBnV*ad,V#pZU1b;KJJkIS5'.sDk.,%Hb",DHQ=\8qm!Mc'+qetG>-oRYbHaj#<;%UM?$M^k`bZo>8<U,P-Xj2p92%YhI)b2W;@#J+1olT<!^:MGK"qkkO:Wd4.D`)_,uFkYD.-Sa]OtIge4:qWo+k]_,p]Qb7V/Q8G@nGUm_$J:'3c$c;&>&X;G0J7+`1(q1JlYHQ:ZeBcm3.&<E;&a@UoaDk/!!iA%<sm1JlYGP0#m5Q8>Ek;N*Dh@i(`4fP1NnO^5h<Yq](_7F2]Gk'3"\l!'N11@$HSCatB*U%r7Hj-Zm+.eG$_9QHHAQ=m'&s,uCP(*V$Ph0Zt9cZKl`V^pJA5,X>$D4t9U>tA+N+iTbf^k+,^HbiV3?'sco]Vs#u4.DI&ZI+Nj_R8V!1f<6PS%df#$\pgDc#K8k#Zr(#EKB442Z:Vu(?.aqrPq0TP_Y6Sp2rs&e%mt@W4b0s?Bkp"n.8E0^m83t("u[OOk*j*`*j:GmQ6YtCQ@GjaD5DMWKj(qs3#)P2<'YEMW:!3(cYO*<Zt*T@)u8+O9;AU[h0"O%LB\d9O`!uNYpmmM45>G:_k6A8VQ0QL>%aM5tmPQ&e<h<X%G:,_DCB_V,iG1d1VX>@GRhX)=rieMUO]6%iKZLku[ATdcd4E1pnni_\@2+][B\W)MjCI97fqdAg&DDWX@<l34_ZNfV;H_Zll\=XBRBpN&MId9N0@IFVB:*MbUP%@WnKVE%^UGk^ILn'7B3lJ^#AbnNQ[,I#0qslca\agOFO?8J^*]ZSQ`<qnJrpmD[UjhU:X$jlb;Nbj45^)gP+0,YT0"<E&a)fWP=ni@K()'kag@CYcoETlE2X`),UP"D&<WH9D@b&f'V8V3GVXo_O4L'e1b<rJ=Do<nc!3<0esE;Y-!Fl80&JMO/P3(LA'K^B$lb4qWr"#lf(d't2lij8$%/=$A#8pU'(HFb8"JfAQl=[f&-KJ#W(3&Dt@Ia-u\+]QsNapMNH]6f#s)f%kSLqkbD&]rBt`?t_IdcC5aK)WR)@d30_1.Ikm]oWRg-h>Hl)0fUpc.72iF7Q=XWoTUfHkTN/^V7=5Gc^aI9[_I&nTg)6Ud8.r48&F[3B@/&<[C=(.=5bEmhQ.Ed=I]=S[[U"B=RZ"WE/7;%lWkT=7*DAZDct2dF6dN45:6%N)?~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 845
>>
stream
GauHJD+iYL&H;+$QuCMAVjVs?TnjHl"Y$S7j$ANA:K)>VR2;"bqG&q]c^/D5J[`[Rc#/n]k':QNQQY6/`Flmb3(i/i(BP52c@HZZ85J/'h21G!8B,e3B"^qi'AB,L_@p%LOhC4G/n"GK2Df]A#8"*M?`SJ*LPk!7HWXlS[[9XWFA7:d4\1&gqYi,`qiF2snIgusW6*eMF;gH>W5<I$!t9@l,/?X2/ME%J5/?Y@hh=.1#Tj/+<rn1(@V)l/34oPX>eqfETC!8uTeGB%0&!%gmJTg;2F1sa.@ph;?;O&bd>&?$gC;O]l_5:FA`(A1WUn:5#Oq?TG_l47^2XW'\P\^>A_+</]Z@F^W2lje7&fIG@ATPOqF[?<8PiQ`,1P:oZ8@cA=$3l0&nR:qAX&&&:PuT=U:4r#`mD_lHX*rh!d\HPOXE83A27VCWu#E=[-%J\J]7\FG6l"EamRR4mM#jA(6P@D#c+"WCMBl?r4YF^"mYB%,]TYZN9)f6C1n#i6fGEBBT7+P8YXcS[85")80j%K9u_cGjd&:*1>E@)q\uq!jWt8m`Z-4VPACYV*<;!o?%`aELJW"QBLBmWAHk(r)>[qfl\bSVpUY:Gqmq<1IoEbpF6b<(kqchsP0qZ/Q=dFe2Cl0mSlY("W8L=@.=i[?G&(u8#>iRR.CWu\7CNC$2f][k:HM<\(9T'Z/QemD[<G?YKOsA)Nb^8=,[?.hU*UGcZkHb::WT)[[RH4`%[GCMat:M4#NOpsgsUSGP+9[6W=gIn`se<`Rdme[3aZT%NK's=];uu%E'/_Y+f#ef<C0Vu0)68pPhoT4bCn5&5F(BJGpFj;>IXhuIfKgqcK"~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 832
>>
stream
GasbY95i9E'Lqim'pi4Li\Ksm,o.1%6=PM]b=K^a;rj+oOWT,Koit;jRT=;tUt@\rSF=a^kCs)62ijTM&`>^&=78.g!SJK4KaP+]E0KZ5(VjVC)U6.pW6ba.(S68%)S%dJZ6NW0r!?(p(',[c_-Sl$^ZrOegAptj*u]BD$/%IP0BWlRB/)e:'k%5[mVRo-g[(d!i@gV2-GZU[:NU<oY0X_cY(j%;OLK(P<u,G3rsAr-K\%>d(!g@3QOc.f^N\3@-4S5OGR78&jP&n\L2t#rcj?a0LPJqdTo:I)&15)tMs@f+b;B173&m"8*U=Fq8tiN<)g%OJKq68)/l=[sf%ug3H'JphN=]mobXSeqBVgfIG@I%Jc-S54e2t"aCDennP'*/FVUp-)RauQIS_30!%[GBbnP3oDX-lDq\64/kFri/e6#1aDn'!]';PR5Ed>hnIC-!Hln\dJn9rjtYpIZgeM2bgh;&NpXn&.,t?D(3?-f0]Na%FY)nYFqHD-;jkI<S_oXYUouo3kg:-ar8>*k@M,[5/c8`hB8nQ$=[bl*[neNlf4$Z!m"Ks.4.';@2u%nCX0n<c-I\`R&^1\9crqI+0lm:C2_En!&dJ4gu1j+7b$;?/EK(/kJ`:S`lrG#gcCE_``YgR_&MbQi0?hIKA<l0-PGTJ@p^ZZ\@k'd'#oDmk<-&+4i:9qnl>.._q_DlLGLZW_7)cqhnPjEQ.&E*1CLrKXYWjF\>9V`/nD*&oPn$bH$6_L[V,C9q\H^V@2d4hWZH4hKQ6cclC5Dd-!*c_ejm0cT%&BS+^2"JM?,j*rhk\5G,Qnb%[!@i7n05A\d^UQPRA0~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1422
>>
stream
Gau0B?#SIW&:ErMfXGo3#pY<?,o%g<\b6(E&-TPE+8nN![#\jJKbTMOIslMi\$G%iDK7<1,dq1./8&U.aW\MC*e'Fb_!c?S]]R">R'2#f/:mU+r%W[7T#^UO4I7UkT(<sj.[_jIJjYj%N#/dMGltCbc<F+fXY'1QG>NL\4bNRF0`Q8;QNpd3ic!f8X#u@S_S*StO1mFi@?8fJE7O]j.Fhh;eq%+_P\Vhk6)A"2EI7Sm*'AZa!1kScMc_SXb)hI5nP$m[O:.Xkn@($^[T1=-eWq,R?\kYfB(/hgUK[-MNBUl7`f6`"o&G4;5a82bMm^XQ+Arr7!fPaSe;ZPF`]J81of;ZQX1T>*ip7@PPd3E;q*anlW--k*_3E;Ci=u]#,=P`ckKIZg+'$^e>#e%a-BSIE1)9$p#pBhjquYu+eNft8/(>'b-r[`VTNT9t4;-!*^/Ce)odQ<*'MLn4T4%)=0t,)!D*\5dSp.WD]/@_-9%V3fZ6Y^gMd?4Hf5@rH[@V,9ebT:gI#hV:i3P&-MMhV\Z=7V^B5k7Ym@`%ZCr"el9Eu&V,RTZ"RMSXeEpOD=N,COJ?Y`c""LFt@HE5d-7mdgI,Pj'aZI/'4mF2$'#dT[@6LY)/469kL*@;1i+/2kkGd>kQ(`6,bB8ePG9?uP(a:Eg)]/FoBeZ^mAKq:W56BtT[CBm3OC@PSfcDaN2LJS0s4Q2[V/P2.b7;,RKi!4Bb'eq5K#f/NZ);LV!%-jn&r_K/3cE\^Q&(J06\^a`IfF6lbcM,7TTmS#0*MuHZ)=30Z92CISkkq*o%&f+(BT/QXQ\daq==SN!k,ZIClc]u9VYn=qUKVDJdFsY.EeKkq&IU8c;/\gbCWsU\do"8;3fF@Q[=VsD`>/P'3!)"9n2&2G3(a_O0sr?`p#KS&R57-lOMLYCU",g1;B5YHpSKjlELXYeBo.dOP[s:pFBX:Yc*Pf2K#)e==(1fGJo0GT=4n!0SRkZ?30Y@!CtD%X#P?GR[X#n0BCH\lA-#e9gkP!"NPkZ($ch!0ePl#IZnL".dNIKP9B/]07D))W<CkbX>E.JK97qE?!G1HMb;aK#M_mH<cM'<!g=/ZK*ZfKk.>+L=47bJTd4[PIFM@iu8=0DuWb_+iis13LS?FXK:5snA(!^lL(pb;G^W"*\UC`gGQulgZNpDmD,<C.[<j`6,PX^*(mm*C1H4Cg&m.#=MpGs%&g0>oo`ToA94][:aefW/[[p1mi-Q&UI4GrZAUXl<mT>759(^tl++*@sBQ\pm',`5Ucc-QQN+2k4Dhr7(9^B<ie%SW[<c>oM;1&$5SLe(0%%/[EqlVPYpHg3A>K]psR%ce&sJ6--#jJPi*^$c@kefZ=HE,ct8&BfZ62$1!#'Ru?sG)'Z6I[RtT>[_qqa(@PWVg]A*r%l1.cNI$=J]!u'N7!,N~>endstream
endobj
xref
0 16
0000000000 65535 f
0000000073 00000 n
0000000124 00000 n
0000000231 00000 n
0000000343 00000 n
0000000426 00000 n
0000000631 00000 n
0000000836 00000 n
0000001041 00000 n
0000001246 00000 n
0000001315 00000 n
0000001612 00000 n
0000001690 00000 n
0000004191 00000 n
0000005127 00000 n
0000006050 00000 n
trailer
<<
/ID
[<2232646b4bd42a525eb8afd576232bc1><2232646b4bd42a525eb8afd576232bc1>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 10 0 R
/Root 9 0 R
/Size 16
>>
startxref
7564
%%EOF

Binary file not shown.

View file

@ -0,0 +1,561 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🗺️ Classification k-NN : Haute-Corse ou Corse du Sud ?\n",
"\n",
"## Objectif\n",
"Utiliser l'algorithme des **k plus proches voisins (k-NN)** pour déterminer si un point de la carte de Corse se situe en **Haute-Corse (2B)** ou en **Corse du Sud (2A)**, en se basant sur les villages les plus proches.\n",
"\n",
"## Principe\n",
"1. On charge les données des villages corses avec leurs coordonnées GPS et leur département\n",
"2. On choisit un point sur la carte\n",
"3. On calcule les distances entre ce point et tous les villages\n",
"4. On identifie les k villages les plus proches\n",
"5. On vote : le département majoritaire parmi ces k villages devient la prédiction"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📦 Installation et imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Installation des bibliothèques nécessaires (si besoin)\n",
"import sys\n",
"!{sys.executable} -m pip install folium pandas numpy -q"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import folium\n",
"from folium.plugins import MarkerCluster\n",
"import math\n",
"from collections import Counter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📊 Chargement des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Charger le fichier CSV\n",
"# Remplacez 'villages_corse.csv' par le chemin de votre fichier\n",
"df = pd.read_csv('villages_corse.csv', sep=';', encoding='utf-8')\n",
"\n",
"# Afficher les premières lignes\n",
"print(f\"Nombre de villages : {len(df)}\")\n",
"df.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🔧 Préparation des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def parse_coordinates(point_geo_str):\n",
" \"\"\"\n",
" Parse la colonne Point_Geo pour extraire latitude et longitude.\n",
" Format attendu : \"POINT(longitude latitude)\" ou similaire\n",
" \"\"\"\n",
" try:\n",
" # Supprimer 'POINT(' et ')'\n",
" coords = point_geo_str.replace('POINT(', '').replace(')', '').strip()\n",
" lon, lat = coords.split()\n",
" return float(lat), float(lon)\n",
" except:\n",
" return None, None\n",
"\n",
"# Extraire les coordonnées\n",
"df[['latitude', 'longitude']] = df['Point_Geo'].apply(\n",
" lambda x: pd.Series(parse_coordinates(x))\n",
")\n",
"\n",
"# Supprimer les lignes sans coordonnées valides\n",
"df = df.dropna(subset=['latitude', 'longitude'])\n",
"\n",
"# Simplifier les noms de départements\n",
"df['dept_simple'] = df['Code Département'].apply(lambda x: '2A' if x == '2A' else '2B')\n",
"\n",
"print(f\"Villages avec coordonnées valides : {len(df)}\")\n",
"print(f\"\\nRépartition par département :\")\n",
"print(df['dept_simple'].value_counts())\n",
"\n",
"df[['Nom français', 'dept_simple', 'latitude', 'longitude']].head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📏 Fonction de calcul de distance\n",
"\n",
"Nous utilisons la **formule de Haversine** pour calculer la distance entre deux points GPS sur la surface de la Terre."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def haversine_distance(lat1, lon1, lat2, lon2):\n",
" \"\"\"\n",
" Calcule la distance en kilomètres entre deux points GPS.\n",
" Formule de Haversine.\n",
" \"\"\"\n",
" R = 6371 # Rayon de la Terre en km\n",
" \n",
" # Conversion en radians\n",
" lat1_rad = math.radians(lat1)\n",
" lat2_rad = math.radians(lat2)\n",
" delta_lat = math.radians(lat2 - lat1)\n",
" delta_lon = math.radians(lon2 - lon1)\n",
" \n",
" # Formule de Haversine\n",
" a = math.sin(delta_lat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2\n",
" c = 2 * math.asin(math.sqrt(a))\n",
" \n",
" return R * c\n",
"\n",
"# Test de la fonction\n",
"# Distance entre Ajaccio et Bastia (environ 100 km)\n",
"dist_test = haversine_distance(41.9267, 8.7369, 42.7028, 9.4500)\n",
"print(f\"Distance Ajaccio-Bastia : {dist_test:.1f} km\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎯 Algorithme k-NN"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def knn_classify(test_lat, test_lon, df, k=5):\n",
" \"\"\"\n",
" Classifie un point (test_lat, test_lon) en utilisant k-NN.\n",
" \n",
" Retourne :\n",
" - prediction : le département prédit ('2A' ou '2B')\n",
" - neighbors : DataFrame des k plus proches voisins\n",
" - votes : dictionnaire des votes\n",
" \"\"\"\n",
" # Calculer les distances pour tous les villages\n",
" distances = []\n",
" for idx, row in df.iterrows():\n",
" dist = haversine_distance(test_lat, test_lon, row['latitude'], row['longitude'])\n",
" distances.append({\n",
" 'village': row['Nom français'],\n",
" 'departement': row['dept_simple'],\n",
" 'latitude': row['latitude'],\n",
" 'longitude': row['longitude'],\n",
" 'distance': dist\n",
" })\n",
" \n",
" # Créer un DataFrame et trier par distance\n",
" dist_df = pd.DataFrame(distances)\n",
" dist_df = dist_df.sort_values('distance')\n",
" \n",
" # Sélectionner les k plus proches\n",
" neighbors = dist_df.head(k)\n",
" \n",
" # Voter\n",
" votes = Counter(neighbors['departement'])\n",
" prediction = votes.most_common(1)[0][0]\n",
" \n",
" return prediction, neighbors, votes\n",
"\n",
"# Test de l'algorithme avec un point au centre de la Corse\n",
"test_lat, test_lon = 42.15, 9.05\n",
"k = 5\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
"\n",
"print(f\"\\n🎯 Point de test : ({test_lat}, {test_lon})\")\n",
"print(f\"\\nAvec k={k} :\")\n",
"print(f\"Prédiction : {prediction}\")\n",
"print(f\"Votes : {dict(votes)}\")\n",
"print(f\"\\nLes {k} plus proches voisins :\")\n",
"print(neighbors[['village', 'departement', 'distance']])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🗺️ Visualisation avec Folium"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def create_map(test_lat=None, test_lon=None, k=5, show_all_villages=False):\n",
" \"\"\"\n",
" Crée une carte interactive avec Folium.\n",
" \"\"\"\n",
" # Centre de la Corse\n",
" center_lat = 42.15\n",
" center_lon = 9.05\n",
" \n",
" # Créer la carte\n",
" m = folium.Map(\n",
" location=[center_lat, center_lon],\n",
" zoom_start=9,\n",
" tiles='OpenStreetMap'\n",
" )\n",
" \n",
" # Afficher tous les villages (optionnel, peut être lourd)\n",
" if show_all_villages:\n",
" marker_cluster = MarkerCluster().add_to(m)\n",
" \n",
" for idx, row in df.iterrows():\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" folium.CircleMarker(\n",
" location=[row['latitude'], row['longitude']],\n",
" radius=2,\n",
" color=color,\n",
" fill=True,\n",
" fillColor=color,\n",
" fillOpacity=0.3,\n",
" popup=f\"{row['Nom français']} ({row['dept_simple']})\"\n",
" ).add_to(marker_cluster)\n",
" \n",
" # Si un point de test est fourni\n",
" if test_lat is not None and test_lon is not None:\n",
" # Classification\n",
" prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
" \n",
" # Marqueur pour le point de test\n",
" color = 'darkred' if prediction == '2A' else 'darkblue'\n",
" folium.Marker(\n",
" location=[test_lat, test_lon],\n",
" popup=f\"Point à classifier<br>Prédiction : {prediction}<br>Votes : {dict(votes)}\",\n",
" icon=folium.Icon(color=color, icon='star', prefix='fa')\n",
" ).add_to(m)\n",
" \n",
" # Afficher les k plus proches voisins\n",
" for idx, neighbor in neighbors.iterrows():\n",
" # Marqueur pour chaque voisin\n",
" color = 'red' if neighbor['departement'] == '2A' else 'blue'\n",
" folium.Marker(\n",
" location=[neighbor['latitude'], neighbor['longitude']],\n",
" popup=f\"{neighbor['village']}<br>{neighbor['departement']}<br>Distance: {neighbor['distance']:.2f} km\",\n",
" icon=folium.Icon(color=color, icon='info-sign')\n",
" ).add_to(m)\n",
" \n",
" # Ligne entre le point test et le voisin\n",
" folium.PolyLine(\n",
" locations=[\n",
" [test_lat, test_lon],\n",
" [neighbor['latitude'], neighbor['longitude']]\n",
" ],\n",
" color=color,\n",
" weight=2,\n",
" opacity=0.5\n",
" ).add_to(m)\n",
" \n",
" # Légende\n",
" legend_html = '''\n",
" <div style=\"position: fixed; \n",
" bottom: 50px; right: 50px; width: 200px; height: 120px; \n",
" background-color: white; border:2px solid grey; z-index:9999; \n",
" font-size:14px; padding: 10px\">\n",
" <p><strong>Légende</strong></p>\n",
" <p><i class=\"fa fa-circle\" style=\"color:red\"></i> Corse du Sud (2A)</p>\n",
" <p><i class=\"fa fa-circle\" style=\"color:blue\"></i> Haute-Corse (2B)</p>\n",
" <p><i class=\"fa fa-star\" style=\"color:darkred\"></i> Point à classifier</p>\n",
" </div>\n",
" '''\n",
" m.get_root().html.add_child(folium.Element(legend_html))\n",
" \n",
" return m\n",
"\n",
"# Créer la carte avec le point de test\n",
"map_with_test = create_map(test_lat=42.15, test_lon=9.05, k=5, show_all_villages=False)\n",
"map_with_test"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🔬 Expérimentation : Influence de k"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Test avec différentes valeurs de k\n",
"test_point = (42.15, 9.05) # Point au centre de la Corse\n",
"\n",
"print(f\"Point testé : {test_point}\\n\")\n",
"print(f\"{'k':<5} {'Prédiction':<12} {'Votes 2A':<10} {'Votes 2B':<10}\")\n",
"print(\"-\" * 45)\n",
"\n",
"for k in [1, 3, 5, 7, 9, 15]:\n",
" prediction, neighbors, votes = knn_classify(test_point[0], test_point[1], df, k=k)\n",
" votes_2a = votes.get('2A', 0)\n",
" votes_2b = votes.get('2B', 0)\n",
" print(f\"{k:<5} {prediction:<12} {votes_2a:<10} {votes_2b:<10}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎮 Mode interactif : Testez vos propres points !\n",
"\n",
"Modifiez les coordonnées ci-dessous pour tester différents points de la Corse."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# === MODIFIEZ CES VALEURS ===\n",
"test_latitude = 42.5 # Entre 41.3 (sud) et 43.0 (nord)\n",
"test_longitude = 9.2 # Entre 8.5 (ouest) et 9.5 (est)\n",
"k_value = 7 # Nombre de voisins\n",
"# =============================\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_latitude, test_longitude, df, k=k_value)\n",
"\n",
"print(f\"📍 Point : ({test_latitude}, {test_longitude})\")\n",
"print(f\"🔢 k = {k_value}\")\n",
"print(f\"\\n🎯 Prédiction : {'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'}\")\n",
"print(f\"\\n📊 Votes : {dict(votes)}\")\n",
"print(f\"\\n🏘 Les {k_value} plus proches villages :\")\n",
"print(neighbors[['village', 'departement', 'distance']].to_string(index=False))\n",
"\n",
"# Afficher la carte\n",
"map_interactive = create_map(test_latitude, test_longitude, k=k_value, show_all_villages=False)\n",
"map_interactive"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🌍 Carte complète avec tous les villages"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Afficher tous les villages de Corse (peut être lent)\n",
"# Attention : cette cellule peut prendre du temps à s'exécuter\n",
"\n",
"map_all = create_map(show_all_villages=True)\n",
"map_all"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📈 Visualisation de la frontière entre départements"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Créer une grille de points et classifier chacun\n",
"# Cela permet de visualiser la \"frontière\" selon k-NN\n",
"\n",
"def create_decision_boundary_map(k=5, grid_resolution=50):\n",
" \"\"\"\n",
" Crée une carte montrant la frontière de décision de k-NN.\n",
" \"\"\"\n",
" # Limites de la Corse\n",
" lat_min, lat_max = 41.3, 43.0\n",
" lon_min, lon_max = 8.5, 9.6\n",
" \n",
" # Créer une grille\n",
" lats = np.linspace(lat_min, lat_max, grid_resolution)\n",
" lons = np.linspace(lon_min, lon_max, grid_resolution)\n",
" \n",
" m = folium.Map(\n",
" location=[42.15, 9.05],\n",
" zoom_start=8,\n",
" tiles='OpenStreetMap'\n",
" )\n",
" \n",
" # Classifier chaque point de la grille\n",
" print(\"Classification de la grille en cours...\")\n",
" for i, lat in enumerate(lats[::5]): # Réduire la résolution pour la vitesse\n",
" for lon in lons[::5]:\n",
" prediction, _, _ = knn_classify(lat, lon, df, k=k)\n",
" color = 'red' if prediction == '2A' else 'blue'\n",
" \n",
" folium.CircleMarker(\n",
" location=[lat, lon],\n",
" radius=3,\n",
" color=color,\n",
" fill=True,\n",
" fillColor=color,\n",
" fillOpacity=0.2,\n",
" weight=0\n",
" ).add_to(m)\n",
" \n",
" if (i+1) % 5 == 0:\n",
" print(f\" {(i+1)*100//len(lats[::5])}% complété\")\n",
" \n",
" print(\"Terminé !\")\n",
" return m\n",
"\n",
"# Créer la carte (peut prendre quelques secondes)\n",
"print(f\"Création de la carte de frontière avec k=5...\")\n",
"boundary_map = create_decision_boundary_map(k=5, grid_resolution=30)\n",
"boundary_map"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎓 Questions de réflexion\n",
"\n",
"1. **Influence de k** : Testez différentes valeurs de k (1, 3, 5, 10, 20). Comment la prédiction change-t-elle ?\n",
"\n",
"2. **Points frontières** : Trouvez des points où la classification change selon la valeur de k.\n",
"\n",
"3. **Zones ambiguës** : Où se situent les zones les plus difficiles à classifier ?\n",
"\n",
"4. **Validité** : Cette méthode est-elle toujours fiable ? Dans quels cas pourrait-elle échouer ?\n",
"\n",
"5. **Amélioration** : Comment pourrait-on améliorer l'algorithme ? (pondération par distance, normalisation, etc.)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 💡 Pour aller plus loin\n",
"\n",
"### Exercices supplémentaires :\n",
"\n",
"1. **Pondération par distance** : Modifier l'algorithme pour donner plus de poids aux villages plus proches\n",
"2. **Validation croisée** : Tester la précision en utilisant les villages eux-mêmes comme points de test\n",
"3. **Autres critères** : Ajouter l'altitude comme dimension supplémentaire\n",
"4. **Clustering** : Identifier des groupes de villages similaires"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# BONUS : Validation croisée\n",
"# Tester la précision en utilisant les villages eux-mêmes\n",
"\n",
"def cross_validation(df, k=5, sample_size=100):\n",
" \"\"\"\n",
" Teste la précision de k-NN en utilisant un échantillon de villages.\n",
" \"\"\"\n",
" # Prendre un échantillon aléatoire\n",
" sample = df.sample(n=min(sample_size, len(df)), random_state=42)\n",
" \n",
" correct = 0\n",
" total = 0\n",
" \n",
" for idx, row in sample.iterrows():\n",
" # Créer un dataset sans ce village\n",
" df_without = df.drop(idx)\n",
" \n",
" # Classifier ce village\n",
" prediction, _, _ = knn_classify(\n",
" row['latitude'], \n",
" row['longitude'], \n",
" df_without, \n",
" k=k\n",
" )\n",
" \n",
" if prediction == row['dept_simple']:\n",
" correct += 1\n",
" total += 1\n",
" \n",
" accuracy = (correct / total) * 100\n",
" return accuracy, correct, total\n",
"\n",
"print(\"Test de précision de l'algorithme k-NN...\\n\")\n",
"\n",
"for k in [1, 3, 5, 10]:\n",
" accuracy, correct, total = cross_validation(df, k=k, sample_size=100)\n",
" print(f\"k={k:2d} : {accuracy:.1f}% de précision ({correct}/{total} corrects)\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View file

@ -0,0 +1,375 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🗺️ k-NN Corse : Version Interactive avec Clic sur Carte\n",
"\n",
"## 🎮 Mode d'emploi\n",
"1. Exécutez toutes les cellules\n",
"2. **Cliquez sur la carte** pour choisir un point\n",
"3. Ajustez k avec le curseur\n",
"4. Observez la classification en temps réel !"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📦 Installation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Installation des bibliothèques\n",
"import sys\n",
"!{sys.executable} -m pip install ipyleaflet ipywidgets pandas numpy -q"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import math\n",
"from collections import Counter\n",
"from ipyleaflet import Map, Marker, CircleMarker, Polyline, AwesomeIcon, LayerGroup\n",
"from ipywidgets import HTML, VBox, HBox, IntSlider, Output, Label\n",
"from IPython.display import display, clear_output"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📊 Chargement des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Charger les données\n",
"df = pd.read_csv('villages_corse.csv', sep='\\t', encoding='utf-8')\n",
"\n",
"def parse_coordinates(point_geo_str):\n",
" try:\n",
" parts = str(point_geo_str).split(',')\n",
" lat = float(parts[0].strip())\n",
" lon = float(parts[1].strip())\n",
" return lat, lon\n",
" except:\n",
" return None, None\n",
"\n",
"df[['latitude', 'longitude']] = df['Point_Geo'].apply(\n",
" lambda x: pd.Series(parse_coordinates(x))\n",
")\n",
"\n",
"df = df.dropna(subset=['latitude', 'longitude'])\n",
"df['dept_simple'] = df['Code Département'].apply(lambda x: '2A' if str(x) == '2A' else '2B')\n",
"\n",
"print(f\"✅ {len(df)} villages chargés\")\n",
"print(f\" - Corse du Sud (2A) : {len(df[df['dept_simple']=='2A'])}\")\n",
"print(f\" - Haute-Corse (2B) : {len(df[df['dept_simple']=='2B'])}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🧮 Fonctions k-NN"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def haversine_distance(lat1, lon1, lat2, lon2):\n",
" \"\"\"Calcule la distance en km entre deux points GPS.\"\"\"\n",
" R = 6371\n",
" lat1_rad = math.radians(lat1)\n",
" lat2_rad = math.radians(lat2)\n",
" delta_lat = math.radians(lat2 - lat1)\n",
" delta_lon = math.radians(lon2 - lon1)\n",
" \n",
" a = math.sin(delta_lat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2\n",
" c = 2 * math.asin(math.sqrt(a))\n",
" \n",
" return R * c\n",
"\n",
"def knn_classify(test_lat, test_lon, df, k=5):\n",
" \"\"\"Classifie un point avec k-NN.\"\"\"\n",
" distances = []\n",
" for idx, row in df.iterrows():\n",
" dist = haversine_distance(test_lat, test_lon, row['latitude'], row['longitude'])\n",
" distances.append({\n",
" 'village': row['Nom français'],\n",
" 'nom_corse': row['Nom corse'],\n",
" 'departement': row['dept_simple'],\n",
" 'latitude': row['latitude'],\n",
" 'longitude': row['longitude'],\n",
" 'distance': dist\n",
" })\n",
" \n",
" dist_df = pd.DataFrame(distances).sort_values('distance')\n",
" neighbors = dist_df.head(k)\n",
" votes = Counter(neighbors['departement'])\n",
" prediction = votes.most_common(1)[0][0]\n",
" \n",
" return prediction, neighbors, votes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🗺️ Carte Interactive\n",
"\n",
"**Instructions :**\n",
"- 🖱️ **Cliquez sur la carte** pour placer un point\n",
"- 🎚️ **Ajustez k** avec le curseur\n",
"- 👁️ La classification se met à jour automatiquement"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Créer la carte\n",
"m = Map(center=(42.15, 9.05), zoom=9, scroll_wheel_zoom=True)\n",
"\n",
"# Couches pour les éléments dynamiques\n",
"test_point_layer = LayerGroup()\n",
"neighbors_layer = LayerGroup()\n",
"lines_layer = LayerGroup()\n",
"\n",
"m.add_layer(test_point_layer)\n",
"m.add_layer(neighbors_layer)\n",
"m.add_layer(lines_layer)\n",
"\n",
"# Afficher quelques villages de référence\n",
"sample_villages = df.sample(n=min(50, len(df)), random_state=42)\n",
"for idx, row in sample_villages.iterrows():\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" circle = CircleMarker(\n",
" location=(row['latitude'], row['longitude']),\n",
" radius=3,\n",
" color=color,\n",
" fill_color=color,\n",
" fill_opacity=0.4,\n",
" weight=1\n",
" )\n",
" m.add_layer(circle)\n",
"\n",
"# Widget pour k\n",
"k_slider = IntSlider(\n",
" value=5,\n",
" min=1,\n",
" max=20,\n",
" step=1,\n",
" description='k:',\n",
" continuous_update=False\n",
")\n",
"\n",
"# Zone de résultats\n",
"result_output = Output()\n",
"info_html = HTML(value=\"<p style='font-size:16px; padding:10px; background:#f0f0f0; border-radius:5px;'>👆 <b>Cliquez sur la carte pour classifier un point</b></p>\")\n",
"\n",
"# Variable globale pour stocker les coordonnées\n",
"current_coords = {'lat': None, 'lon': None}\n",
"\n",
"def update_classification(lat, lon, k):\n",
" \"\"\"Met à jour la classification et la visualisation.\"\"\"\n",
" # Effacer les couches précédentes\n",
" test_point_layer.clear_layers()\n",
" neighbors_layer.clear_layers()\n",
" lines_layer.clear_layers()\n",
" \n",
" # Classification\n",
" prediction, neighbors, votes = knn_classify(lat, lon, df, k=k)\n",
" \n",
" # Couleur selon prédiction\n",
" color = 'red' if prediction == '2A' else 'blue'\n",
" dept_name = 'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'\n",
" \n",
" # Marqueur du point test\n",
" icon = AwesomeIcon(\n",
" name='star',\n",
" marker_color='darkred' if prediction == '2A' else 'darkblue',\n",
" icon_color='white'\n",
" )\n",
" test_marker = Marker(location=(lat, lon), icon=icon, draggable=False)\n",
" test_point_layer.add_layer(test_marker)\n",
" \n",
" # Afficher les k plus proches voisins\n",
" for idx, neighbor in neighbors.iterrows():\n",
" n_color = 'red' if neighbor['departement'] == '2A' else 'blue'\n",
" \n",
" # Marqueur du voisin\n",
" n_marker = CircleMarker(\n",
" location=(neighbor['latitude'], neighbor['longitude']),\n",
" radius=8,\n",
" color=n_color,\n",
" fill_color=n_color,\n",
" fill_opacity=0.7,\n",
" weight=2\n",
" )\n",
" neighbors_layer.add_layer(n_marker)\n",
" \n",
" # Ligne vers le voisin\n",
" line = Polyline(\n",
" locations=[\n",
" (lat, lon),\n",
" (neighbor['latitude'], neighbor['longitude'])\n",
" ],\n",
" color=n_color,\n",
" weight=2,\n",
" opacity=0.5\n",
" )\n",
" lines_layer.add_layer(line)\n",
" \n",
" # Afficher les résultats\n",
" with result_output:\n",
" clear_output(wait=True)\n",
" print(f\"📍 Coordonnées : ({lat:.4f}, {lon:.4f})\")\n",
" print(f\"🔢 k = {k}\")\n",
" print(f\"\\n🎯 Prédiction : {dept_name}\")\n",
" print(f\"📊 Votes : 2A={votes.get('2A', 0)}, 2B={votes.get('2B', 0)}\")\n",
" print(f\"\\n🏘 Les {k} plus proches villages :\")\n",
" print(neighbors[['village', 'nom_corse', 'departement', 'distance']].to_string(index=False))\n",
" \n",
" # Mettre à jour l'info\n",
" info_html.value = f\"<div style='font-size:16px; padding:10px; background:{'#ffebee' if prediction=='2A' else '#e3f2fd'}; border-radius:5px; border-left: 4px solid {color};'><b>Classification : {dept_name}</b><br>Votes : 2A={votes.get('2A', 0)}, 2B={votes.get('2B', 0)}</div>\"\n",
"\n",
"def handle_click(**kwargs):\n",
" \"\"\"Gestionnaire de clic sur la carte.\"\"\"\n",
" if kwargs.get('type') == 'click':\n",
" coords = kwargs.get('coordinates')\n",
" lat, lon = coords\n",
" current_coords['lat'] = lat\n",
" current_coords['lon'] = lon\n",
" update_classification(lat, lon, k_slider.value)\n",
"\n",
"def on_k_change(change):\n",
" \"\"\"Gestionnaire de changement de k.\"\"\"\n",
" if current_coords['lat'] is not None:\n",
" update_classification(current_coords['lat'], current_coords['lon'], change['new'])\n",
"\n",
"# Connecter les événements\n",
"m.on_interaction(handle_click)\n",
"k_slider.observe(on_k_change, names='value')\n",
"\n",
"# Afficher l'interface\n",
"display(VBox([\n",
" info_html,\n",
" HBox([Label('Nombre de voisins (k):'), k_slider]),\n",
" m,\n",
" result_output\n",
"]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎯 Points d'intérêt à tester\n",
"\n",
"Essayez de cliquer sur ces zones :\n",
"\n",
"- **Ajaccio** : (41.9267, 8.7369) - Capitale 2A\n",
"- **Bastia** : (42.7028, 9.4500) - Préfecture 2B\n",
"- **Corte** : (42.3062, 9.1509) - Centre de la Corse\n",
"- **Frontière approximative** : Zone entre 42.0 et 42.3 latitude\n",
"\n",
"### Questions à explorer :\n",
"1. 🤔 Où se situe la \"frontière\" k-NN entre les deux départements ?\n",
"2. 📊 Comment k influence-t-il la classification près de cette frontière ?\n",
"3. 🏔️ Y a-t-il des zones ambiguës où le résultat change souvent ?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 💡 Mode manuel (si la carte ne fonctionne pas)\n",
"\n",
"Si le clic sur carte ne fonctionne pas, utilisez cette cellule :"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Mode manuel : entrez les coordonnées\n",
"test_lat = 42.3 # Modifiez ici\n",
"test_lon = 9.15 # Modifiez ici\n",
"k = 5\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
"dept_name = 'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'\n",
"\n",
"print(f\"📍 Point : ({test_lat}, {test_lon})\")\n",
"print(f\"🎯 Prédiction : {dept_name}\")\n",
"print(f\"📊 Votes : {dict(votes)}\")\n",
"print(f\"\\n🏘 Les {k} plus proches villages :\")\n",
"print(neighbors[['village', 'nom_corse', 'departement', 'distance']].to_string(index=False))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📝 Notes techniques\n",
"\n",
"Cette version utilise **ipyleaflet** qui offre une vraie interactivité bidirectionnelle entre Python et JavaScript dans Jupyter.\n",
"\n",
"**Avantages :**\n",
"- ✅ Clic directement sur la carte\n",
"- ✅ Mise à jour en temps réel\n",
"- ✅ Curseur interactif pour k\n",
"- ✅ Pas besoin de recharger\n",
"\n",
"**Prérequis :**\n",
"- Jupyter Notebook ou JupyterLab\n",
"- Extension widgets activée : `jupyter nbextension enable --py widgetsnbextension`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View file

@ -0,0 +1,375 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🗺️ k-NN Corse : Version Interactive avec Clic sur Carte\n",
"\n",
"## 🎮 Mode d'emploi\n",
"1. Exécutez toutes les cellules\n",
"2. **Cliquez sur la carte** pour choisir un point\n",
"3. Ajustez k avec le curseur\n",
"4. Observez la classification en temps réel !"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📦 Installation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Installation des bibliothèques\n",
"import sys\n",
"!{sys.executable} -m pip install ipyleaflet ipywidgets pandas numpy -q"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import math\n",
"from collections import Counter\n",
"from ipyleaflet import Map, Marker, CircleMarker, Polyline, AwesomeIcon, LayerGroup\n",
"from ipywidgets import HTML, VBox, HBox, IntSlider, Output, Label\n",
"from IPython.display import display, clear_output"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📊 Chargement des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Charger les données\n",
"df = pd.read_csv('villages_corse.csv', sep='\\t', encoding='utf-8')\n",
"\n",
"def parse_coordinates(point_geo_str):\n",
" try:\n",
" parts = str(point_geo_str).split(',')\n",
" lat = float(parts[0].strip())\n",
" lon = float(parts[1].strip())\n",
" return lat, lon\n",
" except:\n",
" return None, None\n",
"\n",
"df[['latitude', 'longitude']] = df['Point_Geo'].apply(\n",
" lambda x: pd.Series(parse_coordinates(x))\n",
")\n",
"\n",
"df = df.dropna(subset=['latitude', 'longitude'])\n",
"df['dept_simple'] = df['Code Département'].apply(lambda x: '2A' if str(x) == '2A' else '2B')\n",
"\n",
"print(f\"✅ {len(df)} villages chargés\")\n",
"print(f\" - Corse du Sud (2A) : {len(df[df['dept_simple']=='2A'])}\")\n",
"print(f\" - Haute-Corse (2B) : {len(df[df['dept_simple']=='2B'])}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🧮 Fonctions k-NN"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def haversine_distance(lat1, lon1, lat2, lon2):\n",
" \"\"\"Calcule la distance en km entre deux points GPS.\"\"\"\n",
" R = 6371\n",
" lat1_rad = math.radians(lat1)\n",
" lat2_rad = math.radians(lat2)\n",
" delta_lat = math.radians(lat2 - lat1)\n",
" delta_lon = math.radians(lon2 - lon1)\n",
" \n",
" a = math.sin(delta_lat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2\n",
" c = 2 * math.asin(math.sqrt(a))\n",
" \n",
" return R * c\n",
"\n",
"def knn_classify(test_lat, test_lon, df, k=5):\n",
" \"\"\"Classifie un point avec k-NN.\"\"\"\n",
" distances = []\n",
" for idx, row in df.iterrows():\n",
" dist = haversine_distance(test_lat, test_lon, row['latitude'], row['longitude'])\n",
" distances.append({\n",
" 'village': row['Nom français'],\n",
" 'nom_corse': row['Nom corse'],\n",
" 'departement': row['dept_simple'],\n",
" 'latitude': row['latitude'],\n",
" 'longitude': row['longitude'],\n",
" 'distance': dist\n",
" })\n",
" \n",
" dist_df = pd.DataFrame(distances).sort_values('distance')\n",
" neighbors = dist_df.head(k)\n",
" votes = Counter(neighbors['departement'])\n",
" prediction = votes.most_common(1)[0][0]\n",
" \n",
" return prediction, neighbors, votes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🗺️ Carte Interactive\n",
"\n",
"**Instructions :**\n",
"- 🖱️ **Cliquez sur la carte** pour placer un point\n",
"- 🎚️ **Ajustez k** avec le curseur\n",
"- 👁️ La classification se met à jour automatiquement"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Créer la carte\n",
"m = Map(center=(42.15, 9.05), zoom=9, scroll_wheel_zoom=True)\n",
"\n",
"# Couches pour les éléments dynamiques\n",
"test_point_layer = LayerGroup()\n",
"neighbors_layer = LayerGroup()\n",
"lines_layer = LayerGroup()\n",
"\n",
"m.add_layer(test_point_layer)\n",
"m.add_layer(neighbors_layer)\n",
"m.add_layer(lines_layer)\n",
"\n",
"# Afficher quelques villages de référence\n",
"sample_villages = df.sample(n=min(50, len(df)), random_state=42)\n",
"for idx, row in sample_villages.iterrows():\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" circle = CircleMarker(\n",
" location=(row['latitude'], row['longitude']),\n",
" radius=3,\n",
" color=color,\n",
" fill_color=color,\n",
" fill_opacity=0.4,\n",
" weight=1\n",
" )\n",
" m.add_layer(circle)\n",
"\n",
"# Widget pour k\n",
"k_slider = IntSlider(\n",
" value=5,\n",
" min=1,\n",
" max=20,\n",
" step=1,\n",
" description='k:',\n",
" continuous_update=False\n",
")\n",
"\n",
"# Zone de résultats\n",
"result_output = Output()\n",
"info_html = HTML(value=\"<p style='font-size:16px; padding:10px; background:#f0f0f0; border-radius:5px;'>👆 <b>Cliquez sur la carte pour classifier un point</b></p>\")\n",
"\n",
"# Variable globale pour stocker les coordonnées\n",
"current_coords = {'lat': None, 'lon': None}\n",
"\n",
"def update_classification(lat, lon, k):\n",
" \"\"\"Met à jour la classification et la visualisation.\"\"\"\n",
" # Effacer les couches précédentes\n",
" test_point_layer.clear_layers()\n",
" neighbors_layer.clear_layers()\n",
" lines_layer.clear_layers()\n",
" \n",
" # Classification\n",
" prediction, neighbors, votes = knn_classify(lat, lon, df, k=k)\n",
" \n",
" # Couleur selon prédiction\n",
" color = 'red' if prediction == '2A' else 'blue'\n",
" dept_name = 'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'\n",
" \n",
" # Marqueur du point test\n",
" icon = AwesomeIcon(\n",
" name='star',\n",
" marker_color='darkred' if prediction == '2A' else 'darkblue',\n",
" icon_color='white'\n",
" )\n",
" test_marker = Marker(location=(lat, lon), icon=icon, draggable=False)\n",
" test_point_layer.add_layer(test_marker)\n",
" \n",
" # Afficher les k plus proches voisins\n",
" for idx, neighbor in neighbors.iterrows():\n",
" n_color = 'red' if neighbor['departement'] == '2A' else 'blue'\n",
" \n",
" # Marqueur du voisin\n",
" n_marker = CircleMarker(\n",
" location=(neighbor['latitude'], neighbor['longitude']),\n",
" radius=8,\n",
" color=n_color,\n",
" fill_color=n_color,\n",
" fill_opacity=0.7,\n",
" weight=2\n",
" )\n",
" neighbors_layer.add_layer(n_marker)\n",
" \n",
" # Ligne vers le voisin\n",
" line = Polyline(\n",
" locations=[\n",
" (lat, lon),\n",
" (neighbor['latitude'], neighbor['longitude'])\n",
" ],\n",
" color=n_color,\n",
" weight=2,\n",
" opacity=0.5\n",
" )\n",
" lines_layer.add_layer(line)\n",
" \n",
" # Afficher les résultats\n",
" with result_output:\n",
" clear_output(wait=True)\n",
" print(f\"📍 Coordonnées : ({lat:.4f}, {lon:.4f})\")\n",
" print(f\"🔢 k = {k}\")\n",
" print(f\"\\n🎯 Prédiction : {dept_name}\")\n",
" print(f\"📊 Votes : 2A={votes.get('2A', 0)}, 2B={votes.get('2B', 0)}\")\n",
" print(f\"\\n🏘 Les {k} plus proches villages :\")\n",
" print(neighbors[['village', 'nom_corse', 'departement', 'distance']].to_string(index=False))\n",
" \n",
" # Mettre à jour l'info\n",
" info_html.value = f\"<div style='font-size:16px; padding:10px; background:{'#ffebee' if prediction=='2A' else '#e3f2fd'}; border-radius:5px; border-left: 4px solid {color};'><b>Classification : {dept_name}</b><br>Votes : 2A={votes.get('2A', 0)}, 2B={votes.get('2B', 0)}</div>\"\n",
"\n",
"def handle_click(**kwargs):\n",
" \"\"\"Gestionnaire de clic sur la carte.\"\"\"\n",
" if kwargs.get('type') == 'click':\n",
" coords = kwargs.get('coordinates')\n",
" lat, lon = coords\n",
" current_coords['lat'] = lat\n",
" current_coords['lon'] = lon\n",
" update_classification(lat, lon, k_slider.value)\n",
"\n",
"def on_k_change(change):\n",
" \"\"\"Gestionnaire de changement de k.\"\"\"\n",
" if current_coords['lat'] is not None:\n",
" update_classification(current_coords['lat'], current_coords['lon'], change['new'])\n",
"\n",
"# Connecter les événements\n",
"m.on_interaction(handle_click)\n",
"k_slider.observe(on_k_change, names='value')\n",
"\n",
"# Afficher l'interface\n",
"display(VBox([\n",
" info_html,\n",
" HBox([Label('Nombre de voisins (k):'), k_slider]),\n",
" m,\n",
" result_output\n",
"]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎯 Points d'intérêt à tester\n",
"\n",
"Essayez de cliquer sur ces zones :\n",
"\n",
"- **Ajaccio** : (41.9267, 8.7369) - Capitale 2A\n",
"- **Bastia** : (42.7028, 9.4500) - Préfecture 2B\n",
"- **Corte** : (42.3062, 9.1509) - Centre de la Corse\n",
"- **Frontière approximative** : Zone entre 42.0 et 42.3 latitude\n",
"\n",
"### Questions à explorer :\n",
"1. 🤔 Où se situe la \"frontière\" k-NN entre les deux départements ?\n",
"2. 📊 Comment k influence-t-il la classification près de cette frontière ?\n",
"3. 🏔️ Y a-t-il des zones ambiguës où le résultat change souvent ?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 💡 Mode manuel (si la carte ne fonctionne pas)\n",
"\n",
"Si le clic sur carte ne fonctionne pas, utilisez cette cellule :"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Mode manuel : entrez les coordonnées\n",
"test_lat = 42.3 # Modifiez ici\n",
"test_lon = 9.15 # Modifiez ici\n",
"k = 5\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
"dept_name = 'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'\n",
"\n",
"print(f\"📍 Point : ({test_lat}, {test_lon})\")\n",
"print(f\"🎯 Prédiction : {dept_name}\")\n",
"print(f\"📊 Votes : {dict(votes)}\")\n",
"print(f\"\\n🏘 Les {k} plus proches villages :\")\n",
"print(neighbors[['village', 'nom_corse', 'departement', 'distance']].to_string(index=False))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📝 Notes techniques\n",
"\n",
"Cette version utilise **ipyleaflet** qui offre une vraie interactivité bidirectionnelle entre Python et JavaScript dans Jupyter.\n",
"\n",
"**Avantages :**\n",
"- ✅ Clic directement sur la carte\n",
"- ✅ Mise à jour en temps réel\n",
"- ✅ Curseur interactif pour k\n",
"- ✅ Pas besoin de recharger\n",
"\n",
"**Prérequis :**\n",
"- Jupyter Notebook ou JupyterLab\n",
"- Extension widgets activée : `jupyter nbextension enable --py widgetsnbextension`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View file

@ -0,0 +1,744 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 🗺️ Classification k-NN : Haute-Corse ou Corse du Sud ?\n",
"\n",
"## Objectif\n",
"Utiliser l'algorithme des **k plus proches voisins (k-NN)** pour déterminer si un point de la carte de Corse se situe en **Haute-Corse (2B)** ou en **Corse du Sud (2A)**, en se basant sur les villages les plus proches.\n",
"\n",
"## Principe\n",
"1. On charge les données des villages corses avec leurs coordonnées GPS et leur département\n",
"2. On choisit un point sur la carte\n",
"3. On calcule les distances entre ce point et tous les villages\n",
"4. On identifie les k villages les plus proches\n",
"5. On vote : le département majoritaire parmi ces k villages devient la prédiction"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📦 Installation et imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Installation des bibliothèques nécessaires (si besoin)\n",
"import sys\n",
"!{sys.executable} -m pip install folium pandas numpy -q"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import folium\n",
"from folium.plugins import MarkerCluster\n",
"import math\n",
"import json\n",
"from collections import Counter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📊 Chargement des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Charger le fichier CSV\n",
"# Remplacez 'villages_corse.csv' par le chemin de votre fichier\n",
"df = pd.read_csv('villages_corse.csv', sep='\\t', encoding='utf-8')\n",
"\n",
"# Afficher les premières lignes\n",
"print(f\"Nombre de villages : {len(df)}\")\n",
"print(f\"\\nColonnes : {list(df.columns)}\")\n",
"df.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🔧 Préparation des données"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def parse_coordinates(point_geo_str):\n",
" \"\"\"\n",
" Parse la colonne Point_Geo pour extraire latitude et longitude.\n",
" Format attendu : \"latitude, longitude\"\n",
" Exemple : \"41.984099158, 8.798384636\"\n",
" \"\"\"\n",
" try:\n",
" # Séparer par la virgule\n",
" parts = str(point_geo_str).split(',')\n",
" lat = float(parts[0].strip())\n",
" lon = float(parts[1].strip())\n",
" return lat, lon\n",
" except Exception as e:\n",
" print(f\"Erreur parsing: {point_geo_str} - {e}\")\n",
" return None, None\n",
"\n",
"# Extraire les coordonnées\n",
"df[['latitude', 'longitude']] = df['Point_Geo'].apply(\n",
" lambda x: pd.Series(parse_coordinates(x))\n",
")\n",
"\n",
"# Supprimer les lignes sans coordonnées valides\n",
"df = df.dropna(subset=['latitude', 'longitude'])\n",
"\n",
"# Simplifier les noms de départements\n",
"df['dept_simple'] = df['Code Département'].apply(lambda x: '2A' if str(x) == '2A' else '2B')\n",
"\n",
"print(f\"Villages avec coordonnées valides : {len(df)}\")\n",
"print(f\"\\nRépartition par département :\")\n",
"print(df['dept_simple'].value_counts())\n",
"\n",
"df[['Nom français', 'dept_simple', 'latitude', 'longitude']].head(10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📏 Fonction de calcul de distance\n",
"\n",
"Nous utilisons la **formule de Haversine** pour calculer la distance entre deux points GPS sur la surface de la Terre."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def haversine_distance(lat1, lon1, lat2, lon2):\n",
" \"\"\"\n",
" Calcule la distance en kilomètres entre deux points GPS.\n",
" Formule de Haversine.\n",
" \"\"\"\n",
" R = 6371 # Rayon de la Terre en km\n",
" \n",
" # Conversion en radians\n",
" lat1_rad = math.radians(lat1)\n",
" lat2_rad = math.radians(lat2)\n",
" delta_lat = math.radians(lat2 - lat1)\n",
" delta_lon = math.radians(lon2 - lon1)\n",
" \n",
" # Formule de Haversine\n",
" a = math.sin(delta_lat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2\n",
" c = 2 * math.asin(math.sqrt(a))\n",
" \n",
" return R * c\n",
"\n",
"# Test de la fonction\n",
"# Distance entre Ajaccio (41.9267, 8.7369) et Bastia (42.7028, 9.4500)\n",
"dist_test = haversine_distance(41.9267, 8.7369, 42.7028, 9.4500)\n",
"print(f\"Distance Ajaccio-Bastia : {dist_test:.1f} km\")\n",
"\n",
"# Test avec Afa et Alando (vos exemples)\n",
"afa = df[df['Nom français'] == 'Afa'].iloc[0]\n",
"alando = df[df['Nom français'] == 'Alando'].iloc[0]\n",
"dist_afa_alando = haversine_distance(afa['latitude'], afa['longitude'], \n",
" alando['latitude'], alando['longitude'])\n",
"print(f\"Distance Afa-Alando : {dist_afa_alando:.1f} km\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎯 Algorithme k-NN"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def knn_classify(test_lat, test_lon, df, k=5):\n",
" \"\"\"\n",
" Classifie un point (test_lat, test_lon) en utilisant k-NN.\n",
" \n",
" Retourne :\n",
" - prediction : le département prédit ('2A' ou '2B')\n",
" - neighbors : DataFrame des k plus proches voisins\n",
" - votes : dictionnaire des votes\n",
" \"\"\"\n",
" # Calculer les distances pour tous les villages\n",
" distances = []\n",
" for idx, row in df.iterrows():\n",
" dist = haversine_distance(test_lat, test_lon, row['latitude'], row['longitude'])\n",
" distances.append({\n",
" 'village': row['Nom français'],\n",
" 'nom_corse': row['Nom corse'],\n",
" 'departement': row['dept_simple'],\n",
" 'latitude': row['latitude'],\n",
" 'longitude': row['longitude'],\n",
" 'distance': dist\n",
" })\n",
" \n",
" # Créer un DataFrame et trier par distance\n",
" dist_df = pd.DataFrame(distances)\n",
" dist_df = dist_df.sort_values('distance')\n",
" \n",
" # Sélectionner les k plus proches\n",
" neighbors = dist_df.head(k)\n",
" \n",
" # Voter\n",
" votes = Counter(neighbors['departement'])\n",
" prediction = votes.most_common(1)[0][0]\n",
" \n",
" return prediction, neighbors, votes\n",
"\n",
"# Test de l'algorithme avec un point au centre de la Corse\n",
"test_lat, test_lon = 42.15, 9.05\n",
"k = 5\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
"\n",
"print(f\"\\n🎯 Point de test : ({test_lat}, {test_lon})\")\n",
"print(f\"\\nAvec k={k} :\")\n",
"print(f\"Prédiction : {'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'}\")\n",
"print(f\"Votes : {dict(votes)}\")\n",
"print(f\"\\nLes {k} plus proches voisins :\")\n",
"print(neighbors[['village', 'nom_corse', 'departement', 'distance']])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🗺️ Visualisation avec Folium"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def create_map(test_lat=None, test_lon=None, k=5, show_all_villages=False, show_boundaries=False):\n",
" \"\"\"\n",
" Crée une carte interactive avec Folium.\n",
" \n",
" Paramètres:\n",
" - test_lat, test_lon: coordonnées du point à tester\n",
" - k: nombre de voisins\n",
" - show_all_villages: afficher tous les villages\n",
" - show_boundaries: afficher les frontières des communes (peut être lent)\n",
" \"\"\"\n",
" # Centre de la Corse\n",
" center_lat = 42.15\n",
" center_lon = 9.05\n",
" \n",
" # Créer la carte\n",
" m = folium.Map(\n",
" location=[center_lat, center_lon],\n",
" zoom_start=9,\n",
" tiles='OpenStreetMap'\n",
" )\n",
" \n",
" # Afficher les frontières des communes (optionnel)\n",
" if show_boundaries:\n",
" print(\"Affichage des frontières des communes...\")\n",
" for idx, row in df.iterrows():\n",
" try:\n",
" zone_geo = json.loads(row['Zone_geo'])\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" \n",
" folium.GeoJson(\n",
" zone_geo,\n",
" style_function=lambda x, color=color: {\n",
" 'fillColor': color,\n",
" 'color': color,\n",
" 'weight': 1,\n",
" 'fillOpacity': 0.1\n",
" },\n",
" tooltip=row['Nom français']\n",
" ).add_to(m)\n",
" except:\n",
" pass\n",
" \n",
" # Afficher tous les villages (optionnel)\n",
" if show_all_villages:\n",
" marker_cluster = MarkerCluster().add_to(m)\n",
" \n",
" for idx, row in df.iterrows():\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" folium.CircleMarker(\n",
" location=[row['latitude'], row['longitude']],\n",
" radius=3,\n",
" color=color,\n",
" fill=True,\n",
" fillColor=color,\n",
" fillOpacity=0.4,\n",
" popup=f\"<b>{row['Nom français']}</b><br>{row['Nom corse']}<br>({row['dept_simple']})\"\n",
" ).add_to(marker_cluster)\n",
" \n",
" # Si un point de test est fourni\n",
" if test_lat is not None and test_lon is not None:\n",
" # Classification\n",
" prediction, neighbors, votes = knn_classify(test_lat, test_lon, df, k=k)\n",
" \n",
" # Marqueur pour le point de test\n",
" color = 'darkred' if prediction == '2A' else 'darkblue'\n",
" dept_name = 'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'\n",
" \n",
" folium.Marker(\n",
" location=[test_lat, test_lon],\n",
" popup=f\"<b>Point à classifier</b><br>Prédiction : {dept_name}<br>Votes : {dict(votes)}\",\n",
" icon=folium.Icon(color=color, icon='star', prefix='fa')\n",
" ).add_to(m)\n",
" \n",
" # Afficher les k plus proches voisins\n",
" for idx, neighbor in neighbors.iterrows():\n",
" # Marqueur pour chaque voisin\n",
" color = 'red' if neighbor['departement'] == '2A' else 'blue'\n",
" folium.Marker(\n",
" location=[neighbor['latitude'], neighbor['longitude']],\n",
" popup=f\"<b>{neighbor['village']}</b><br>{neighbor['nom_corse']}<br>{neighbor['departement']}<br>Distance: {neighbor['distance']:.2f} km\",\n",
" icon=folium.Icon(color=color, icon='info-sign')\n",
" ).add_to(m)\n",
" \n",
" # Ligne entre le point test et le voisin\n",
" folium.PolyLine(\n",
" locations=[\n",
" [test_lat, test_lon],\n",
" [neighbor['latitude'], neighbor['longitude']]\n",
" ],\n",
" color=color,\n",
" weight=2,\n",
" opacity=0.5,\n",
" tooltip=f\"{neighbor['distance']:.2f} km\"\n",
" ).add_to(m)\n",
" \n",
" # Légende\n",
" legend_html = '''\n",
" <div style=\"position: fixed; \n",
" bottom: 50px; right: 50px; width: 220px; height: 130px; \n",
" background-color: white; border:2px solid grey; z-index:9999; \n",
" font-size:14px; padding: 10px\">\n",
" <p><strong>Légende</strong></p>\n",
" <p><i class=\"fa fa-circle\" style=\"color:red\"></i> Corse du Sud (2A)</p>\n",
" <p><i class=\"fa fa-circle\" style=\"color:blue\"></i> Haute-Corse (2B)</p>\n",
" <p><i class=\"fa fa-star\" style=\"color:darkred\"></i> Point à classifier</p>\n",
" </div>\n",
" '''\n",
" m.get_root().html.add_child(folium.Element(legend_html))\n",
" \n",
" return m\n",
"\n",
"# Créer la carte avec le point de test\n",
"map_with_test = create_map(test_lat=42.15, test_lon=9.05, k=5, show_all_villages=False)\n",
"map_with_test"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🔬 Expérimentation : Influence de k"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Test avec différentes valeurs de k\n",
"test_point = (42.15, 9.05) # Point au centre de la Corse\n",
"\n",
"print(f\"Point testé : {test_point}\\n\")\n",
"print(f\"{'k':<5} {'Prédiction':<15} {'Votes 2A':<10} {'Votes 2B':<10}\")\n",
"print(\"-\" * 50)\n",
"\n",
"for k in [1, 3, 5, 7, 9, 15, 21]:\n",
" prediction, neighbors, votes = knn_classify(test_point[0], test_point[1], df, k=k)\n",
" votes_2a = votes.get('2A', 0)\n",
" votes_2b = votes.get('2B', 0)\n",
" dept_name = 'Corse du Sud' if prediction == '2A' else 'Haute-Corse'\n",
" print(f\"{k:<5} {dept_name:<15} {votes_2a:<10} {votes_2b:<10}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎮 Mode interactif : Testez vos propres points !\n",
"\n",
"Modifiez les coordonnées ci-dessous pour tester différents points de la Corse.\n",
"\n",
"**Quelques repères géographiques :**\n",
"- Ajaccio : (41.9267, 8.7369)\n",
"- Bastia : (42.7028, 9.4500)\n",
"- Corte : (42.3062, 9.1509)\n",
"- Porto-Vecchio : (41.5914, 9.2795)\n",
"- Calvi : (42.5679, 8.7575)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# === MODIFIEZ CES VALEURS ===\n",
"test_latitude = 42.3 # Entre 41.3 (sud) et 43.0 (nord)\n",
"test_longitude = 9.15 # Entre 8.5 (ouest) et 9.5 (est)\n",
"k_value = 7 # Nombre de voisins\n",
"# =============================\n",
"\n",
"prediction, neighbors, votes = knn_classify(test_latitude, test_longitude, df, k=k_value)\n",
"\n",
"print(f\"📍 Point : ({test_latitude}, {test_longitude})\")\n",
"print(f\"🔢 k = {k_value}\")\n",
"print(f\"\\n🎯 Prédiction : {'Corse du Sud (2A)' if prediction == '2A' else 'Haute-Corse (2B)'}\")\n",
"print(f\"\\n📊 Votes : {dict(votes)}\")\n",
"print(f\"\\n🏘 Les {k_value} plus proches villages :\")\n",
"print(neighbors[['village', 'nom_corse', 'departement', 'distance']].to_string(index=False))\n",
"\n",
"# Afficher la carte\n",
"map_interactive = create_map(test_latitude, test_longitude, k=k_value, show_all_villages=False)\n",
"map_interactive"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🌍 Carte complète avec tous les villages"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Afficher tous les villages de Corse\n",
"# Note : cette cellule peut prendre quelques secondes à s'exécuter\n",
"\n",
"map_all = create_map(show_all_villages=True, show_boundaries=False)\n",
"map_all"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🗺️ Carte avec frontières des communes (BONUS)\n",
"\n",
"Cette cellule affiche les frontières réelles des communes. **Attention : cela peut prendre du temps à charger !**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Carte avec frontières - peut être lent !\n",
"# Décommentez la ligne suivante pour l'exécuter\n",
"# map_boundaries = create_map(test_lat=42.15, test_lon=9.05, k=5, show_boundaries=True)\n",
"# map_boundaries"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 📈 Visualisation de la frontière de décision"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Créer une grille de points et classifier chacun\n",
"# Cela permet de visualiser la \"frontière\" selon k-NN\n",
"\n",
"def create_decision_boundary_map(k=5, grid_resolution=40):\n",
" \"\"\"\n",
" Crée une carte montrant la frontière de décision de k-NN.\n",
" \"\"\"\n",
" # Limites de la Corse\n",
" lat_min, lat_max = 41.3, 43.0\n",
" lon_min, lon_max = 8.5, 9.6\n",
" \n",
" # Créer une grille\n",
" lats = np.linspace(lat_min, lat_max, grid_resolution)\n",
" lons = np.linspace(lon_min, lon_max, grid_resolution)\n",
" \n",
" m = folium.Map(\n",
" location=[42.15, 9.05],\n",
" zoom_start=8,\n",
" tiles='OpenStreetMap'\n",
" )\n",
" \n",
" # Classifier chaque point de la grille\n",
" print(f\"Classification d'une grille de {grid_resolution}x{grid_resolution} points...\")\n",
" total = len(lats) * len(lons)\n",
" count = 0\n",
" \n",
" for lat in lats:\n",
" for lon in lons:\n",
" prediction, _, _ = knn_classify(lat, lon, df, k=k)\n",
" color = '#ffcccc' if prediction == '2A' else '#ccccff'\n",
" \n",
" folium.CircleMarker(\n",
" location=[lat, lon],\n",
" radius=4,\n",
" color=color,\n",
" fill=True,\n",
" fillColor=color,\n",
" fillOpacity=0.3,\n",
" weight=0\n",
" ).add_to(m)\n",
" \n",
" count += 1\n",
" if count % 100 == 0:\n",
" print(f\" {count}/{total} points traités ({100*count/total:.1f}%)\")\n",
" \n",
" print(\"Terminé !\")\n",
" \n",
" # Ajouter les villages\n",
" for idx, row in df.iterrows():\n",
" color = 'red' if row['dept_simple'] == '2A' else 'blue'\n",
" folium.CircleMarker(\n",
" location=[row['latitude'], row['longitude']],\n",
" radius=2,\n",
" color=color,\n",
" fill=True,\n",
" fillColor=color,\n",
" fillOpacity=0.8,\n",
" popup=row['Nom français']\n",
" ).add_to(m)\n",
" \n",
" return m\n",
"\n",
"# Créer la carte (réduire grid_resolution si c'est trop lent)\n",
"print(f\"Création de la carte de frontière avec k=5...\")\n",
"print(\"Note : cela peut prendre 1-2 minutes...\")\n",
"boundary_map = create_decision_boundary_map(k=5, grid_resolution=30)\n",
"boundary_map"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 💡 Validation de l'algorithme"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# BONUS : Validation croisée\n",
"# Tester la précision en utilisant les villages eux-mêmes\n",
"\n",
"def cross_validation(df, k=5, sample_size=100):\n",
" \"\"\"\n",
" Teste la précision de k-NN en utilisant un échantillon de villages.\n",
" \"\"\"\n",
" # Prendre un échantillon aléatoire\n",
" sample = df.sample(n=min(sample_size, len(df)), random_state=42)\n",
" \n",
" correct = 0\n",
" total = 0\n",
" errors = []\n",
" \n",
" for idx, row in sample.iterrows():\n",
" # Créer un dataset sans ce village\n",
" df_without = df.drop(idx)\n",
" \n",
" # Classifier ce village\n",
" prediction, neighbors, votes = knn_classify(\n",
" row['latitude'], \n",
" row['longitude'], \n",
" df_without, \n",
" k=k\n",
" )\n",
" \n",
" if prediction == row['dept_simple']:\n",
" correct += 1\n",
" else:\n",
" errors.append({\n",
" 'village': row['Nom français'],\n",
" 'vrai_dept': row['dept_simple'],\n",
" 'prediction': prediction,\n",
" 'votes': dict(votes)\n",
" })\n",
" total += 1\n",
" \n",
" accuracy = (correct / total) * 100\n",
" return accuracy, correct, total, errors\n",
"\n",
"print(\"Test de précision de l'algorithme k-NN...\\n\")\n",
"print(\"Validation croisée : chaque village est classifié en fonction de ses voisins.\\n\")\n",
"\n",
"for k in [1, 3, 5, 10, 15]:\n",
" accuracy, correct, total, errors = cross_validation(df, k=k, sample_size=100)\n",
" print(f\"k={k:2d} : {accuracy:.1f}% de précision ({correct}/{total} corrects)\")\n",
"\n",
"# Afficher quelques erreurs pour k=5\n",
"print(\"\\n📋 Exemples d'erreurs avec k=5 :\")\n",
"_, _, _, errors_k5 = cross_validation(df, k=5, sample_size=100)\n",
"if errors_k5:\n",
" for error in errors_k5[:5]:\n",
" print(f\" • {error['village']} : prédit {error['prediction']} (vrai: {error['vrai_dept']}) - votes: {error['votes']}\")\n",
"else:\n",
" print(\" Aucune erreur !\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 🎓 Questions de réflexion\n",
"\n",
"1. **Influence de k** : Comment la prédiction change-t-elle avec différentes valeurs de k ?\n",
"\n",
"2. **Points frontières** : Trouvez des coordonnées où la classification est ambiguë (votes proches).\n",
"\n",
"3. **Zones problématiques** : Où se situent les villages difficiles à classifier correctement ?\n",
"\n",
"4. **Validité géographique** : Cette méthode respecte-t-elle toujours les vraies frontières administratives ?\n",
"\n",
"5. **Améliorations** : Comment pourrait-on améliorer l'algorithme ?\n",
" - Pondération par distance inverse\n",
" - Prise en compte d'autres critères (altitude, population...)\n",
" - k adaptatif selon la densité de villages"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 💡 Exercices supplémentaires\n",
"\n",
"1. **Trouver la frontière** : Trouvez des points sur la \"frontière\" k-NN (là où un changement de k change la classification)\n",
"\n",
"2. **Villages isolés** : Identifiez les villages dont le département diffère de leurs k plus proches voisins\n",
"\n",
"3. **Pondération** : Implémentez une version pondérée où les villages plus proches ont plus d'influence\n",
"\n",
"4. **Comparaison** : Comparez la frontière k-NN avec la vraie frontière administrative"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# EXERCICE : Villages \"anomaliques\"\n",
"# Trouver les villages dont les k plus proches voisins sont majoritairement de l'autre département\n",
"\n",
"def find_anomalous_villages(df, k=5):\n",
" \"\"\"\n",
" Trouve les villages qui seraient mal classifiés par k-NN.\n",
" \"\"\"\n",
" anomalies = []\n",
" \n",
" for idx, row in df.iterrows():\n",
" # Créer un dataset sans ce village\n",
" df_without = df.drop(idx)\n",
" \n",
" # Classifier ce village\n",
" prediction, neighbors, votes = knn_classify(\n",
" row['latitude'], \n",
" row['longitude'], \n",
" df_without, \n",
" k=k\n",
" )\n",
" \n",
" # Si la prédiction ne correspond pas au vrai département\n",
" if prediction != row['dept_simple']:\n",
" anomalies.append({\n",
" 'village': row['Nom français'],\n",
" 'nom_corse': row['Nom corse'],\n",
" 'vrai_dept': row['dept_simple'],\n",
" 'prediction': prediction,\n",
" 'votes_2A': votes.get('2A', 0),\n",
" 'votes_2B': votes.get('2B', 0),\n",
" 'latitude': row['latitude'],\n",
" 'longitude': row['longitude']\n",
" })\n",
" \n",
" return pd.DataFrame(anomalies)\n",
"\n",
"print(\"Recherche des villages 'anomaliques' avec k=5...\\n\")\n",
"anomalies_df = find_anomalous_villages(df, k=5)\n",
"\n",
"print(f\"Nombre de villages anomaliques : {len(anomalies_df)}\")\n",
"print(f\"\\nVillages qui seraient classifiés dans le mauvais département :\\n\")\n",
"print(anomalies_df[['village', 'nom_corse', 'vrai_dept', 'prediction', 'votes_2A', 'votes_2B']])\n",
"\n",
"# Afficher ces villages sur une carte\n",
"if len(anomalies_df) > 0:\n",
" m_anomalies = folium.Map(location=[42.15, 9.05], zoom_start=9)\n",
" \n",
" for idx, row in anomalies_df.iterrows():\n",
" folium.Marker(\n",
" location=[row['latitude'], row['longitude']],\n",
" popup=f\"<b>{row['village']}</b><br>Vrai: {row['vrai_dept']}<br>Prédit: {row['prediction']}<br>Votes: {row['votes_2A']}-{row['votes_2B']}\",\n",
" icon=folium.Icon(color='orange', icon='exclamation-triangle', prefix='fa')\n",
" ).add_to(m_anomalies)\n",
" \n",
" display(m_anomalies)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long