1 Objectif du cas pratique

  • comparer initial vs final avec DOC_VER min/max ;
  • respecter le bon grain à chaque étape ;
  • rester RAM-friendly : sélection de colonnes, agrégation avant jointure ;
  • produire les sorties demandées dans le cas pratique.

Trois niveaux sont utilisés :

  • df_gen : grain = instanceid + doc_ver
  • df_item : grain = instanceid + key_itm_nbr + doc_ver
  • df_tax : grain physique exploité = instanceid + key_itm_nbr + doc_ver + tax_lin_cod

Règle absolue : on conserve doc_ver pendant les agrégations, puis on compare DOC_VER_MIN et DOC_VER_MAX.


2 Terminologie métier de référence

Terme Définition
Déclaration niveau instanceid + doc_ver
Article niveau instanceid + key_itm_nbr + doc_ver
Taxe agrégée article niveau instanceid + key_itm_nbr + doc_ver, après agrégation préalable par tax_lin_cod
Version initiale DOC_VER_MIN
Version finale DOC_VER_MAX
DELTA_TAX TAX_FIN - TAX_INI
Variation fiscale absolue abs(DELTA_TAX)
Workflow normal déclaration avec mainlevée atteinte et stabilité HS / VIT / tax
Signal cas à examiner
owner code déclarant (à rapprocher de dec_cod) — distinct de end_user
exa_exa vérificateur (No examiner si valeur manquante, vide ou égale à None)
end_user acteur technique réel de l’opération, non analysé dans cet atelier

Rappels :

  • les comparaisons initial vs final se font entre DOC_VER_MIN et DOC_VER_MAX ;
  • les analyses HS / VIT / DELTA_TAX se font au grain article ;
  • les taxes détaillées sont d’abord agrégées par tax_lin_cod, en conservant doc_ver.

3 Chargement RAM-friendly

setwd('C:/Users/PC/Desktop/Formation Douanes/ML APPRENTISSAGE/ATELIER SAD HIS/anonymised')
data_dir <- "C:/Users/PC/Desktop/Formation Douanes/ML APPRENTISSAGE/ATELIER SAD HIS/anonymised"

cols_gen <- c(
  "instanceid", "doc_ver", "ide_reg_dat","ide_ast_dat","ide_rcp_dat", "op_date_time", "status", "op_name",
  "cmp_con_cod", "cmp_exp_cod", "cmp_fis_cod", "ide_cuo_cod", "ide_cuo_nam",
  "owner", "ied_id", "exa_exa"
)

cols_item <- c(
  "instanceid", "key_itm_nbr", "doc_ver",
  "tar_hsc_nb1", "tar_hsc_nb2", "vit_stv", "vit_cif",
  "gds_org_cty", "tar_prf"
)

cols_tax <- c(
  "instanceid", "key_itm_nbr", "doc_ver",
  "tax_lin_cod", "tax_lin_bse", "tax_lin_rat", "tax_lin_amt"
)

df_gen <- read_parquet(
  file.path(data_dir, "sad_general_his_anon.parquet"),
  col_select = all_of(cols_gen)
) %>% as.data.frame()

df_item <- read_parquet(
  file.path(data_dir, "sad_item_his_anon.parquet"),
  col_select = all_of(cols_item)
) %>% as.data.frame()

df_tax <- read_parquet(
  file.path(data_dir, "sad_tax_his_anon.parquet"),
  col_select = all_of(cols_tax)
) %>% as.data.frame()

# Filtrage sur l'année 2022 (logique métier : périmètre de l'atelier)
df_gen <- df_gen %>%
  filter(year(ide_reg_dat) == 2022)

valid_instance_ids <- df_gen$instanceid

df_item <- df_item %>%
  filter(instanceid %in% valid_instance_ids)

df_tax <- df_tax %>%
  filter(instanceid %in% valid_instance_ids)

cat("Nombre de déclarations (general) après filtrage :", nrow(df_gen), "\n")
## Nombre de déclarations (general) après filtrage : 675353
cat("Nombre de lignes item après filtrage           :", nrow(df_item), "\n")
## Nombre de lignes item après filtrage           : 1023156
cat("Nombre de lignes tax après filtrage            :", nrow(df_tax), "\n")
## Nombre de lignes tax après filtrage            : 4694107
df_gen <- df_gen %>%
  mutate(
    instanceid   = as.integer(instanceid),
    doc_ver      = as.integer(doc_ver),
    op_date_time = as.POSIXct(op_date_time),
    ide_reg_dat  = as.POSIXct(ide_reg_dat),
    ide_ast_dat  = as.POSIXct(ide_ast_dat),
    ide_rcp_dat  = as.POSIXct(ide_rcp_dat),
    exa_exa      = case_when(
      is.na(exa_exa) ~ "No examiner",
      trimws(as.character(exa_exa)) == "" ~ "No examiner",
      toupper(trimws(as.character(exa_exa))) == "NONE" ~ "No examiner",
      TRUE ~ trimws(as.character(exa_exa))
    ),
    op_name_std  = case_when(
      op_name == "Print Release Order"            ~ "PRINT_RELEASE_ORDER",
      op_name == "Payment"                         ~ "PAYMENT",
      op_name == "Re-route to green"               ~ "RE_ROUTE_GREEN",
      op_name == "Totally exit"                    ~ "TOTALLY_EXIT",
      op_name == "Exit"                            ~ "EXIT",
      op_name == "Exit Paid"                       ~ "EXIT_PAID",
      op_name == "Exit Assessed Credit"            ~ "EXIT_ASSESSED_CREDIT",
      op_name == "Validate and assess"             ~ "VALIDATE_AND_ASSESS",
      op_name == "Assess Registered"               ~ "ASSESS_REGISTERED",
      op_name == "Assess Registered (direct)"      ~ "ASSESS_REGISTERED_DIRECT",
      op_name == "Assessed direct to prepaid"      ~ "ASSESSED_DIRECT_TO_PREPAID",
      op_name == "Assess from selected standby"    ~ "ASSESS_FROM_SELECTED_STANDBY",
      op_name == "Validate"                        ~ "VALIDATE",
      op_name == "Modify Registered"               ~ "MODIFY_REGISTERED",
      op_name == "Post-Entry"                      ~ "POST_ENTRY",
      op_name == "Lock"                            ~ "LOCK",
      op_name == "Unlock"                          ~ "UNLOCK",
      op_name == "Cancel"                          ~ "CANCEL",
      op_name == "Refund"                          ~ "REFUND",
      op_name == "Register T1 on SAD"              ~ "REGISTER_T1_ON_SAD",
      op_name == "Re-route to query"               ~ "RE_ROUTE_QUERY",
      op_name == "Re-route to red"                 ~ "RE_ROUTE_RED",
      op_name == "Export release"                  ~ "EXPORT_RELEASE",
      TRUE ~ str_to_upper(str_replace_all(op_name, "[^A-Za-z0-9]+", "_"))
    ),
    status_std = case_when(
      status %in% c("Assessed", "Assesed") ~ "ASSESSED",
      status == "Paid"                      ~ "PAID",
      status == "Registered"                ~ "REGISTERED",
      status == "Totally exited"            ~ "TOTALLY_EXITED",
      status == "Exited"                    ~ "EXITED",
      status == "Cancelled"                 ~ "CANCELLED",
      TRUE ~ str_to_upper(status)
    )
  ) %>%
  # Logique métier : les déclarations annulées sont exclues de toute analyse
  filter(status_std != "CANCELLED")

df_item <- df_item %>%
  mutate(
    instanceid  = as.integer(instanceid),
    key_itm_nbr = as.integer(key_itm_nbr),
    doc_ver     = as.integer(doc_ver),
    HS          = paste0(coalesce(tar_hsc_nb1, ""), coalesce(tar_hsc_nb2, "")),
    VIT_STV     = as.numeric(vit_stv),
    VIT_CIF     = as.numeric(vit_cif)
  )

df_tax <- df_tax %>%
  mutate(
    instanceid   = as.integer(instanceid),
    key_itm_nbr  = as.integer(key_itm_nbr),
    doc_ver      = as.integer(doc_ver),
    tax_lin_cod  = as.character(tax_lin_cod),
    tax_lin_bse  = as.numeric(tax_lin_bse),
    tax_lin_rat  = as.numeric(tax_lin_rat),
    tax_lin_amt  = as.numeric(tax_lin_amt)
  )

4 Outils

versions <- function(df, keys, version_col = "doc_ver") {
  df %>%
    group_by(across(all_of(keys))) %>%
    summarise(
      DOC_VER_MIN = min(.data[[version_col]], na.rm = TRUE),
      DOC_VER_MAX = max(.data[[version_col]], na.rm = TRUE),
      nb_versions = n_distinct(.data[[version_col]]),
      .groups = "drop"
    )
}

first_last_by_version <- function(df, keys, keep_cols, version_col = "doc_ver") {
  v <- versions(df, keys = keys, version_col = version_col)

  ini <- df %>%
    inner_join(v, by = keys) %>%
    filter(.data[[version_col]] == DOC_VER_MIN) %>%
    select(all_of(keys), all_of(keep_cols)) %>%
    distinct()

  fin <- df %>%
    inner_join(v, by = keys) %>%
    filter(.data[[version_col]] == DOC_VER_MAX) %>%
    select(all_of(keys), all_of(keep_cols)) %>%
    distinct()

  list(versions = v, ini = ini, fin = fin)
}

5 Workflow au grain déclaration

decl_dates <- df_gen %>%
  group_by(instanceid) %>%
  summarise(
    date_depot = {
      x <- ide_reg_dat[!is.na(ide_reg_dat)]
      if (length(x) == 0) as.POSIXct(NA) else min(x)
    },
    date_bae = {
      x <- op_date_time[op_name_std == "PRINT_RELEASE_ORDER" & !is.na(op_date_time)]
      if (length(x) == 0) as.POSIXct(NA) else min(x)
    },
    date_payment = {
      x <- ide_rcp_dat[!is.na(ide_rcp_dat)]
      if (length(x) == 0) as.POSIXct(NA) else min(x)
    },
    op_last = {
      x <- op_date_time[!is.na(op_date_time)]
      if (length(x) == 0) as.POSIXct(NA) else max(x)
    },
    .groups = "drop"
  )

decl_workflow <- versions(df_gen, keys = c("instanceid")) %>%
  left_join(decl_dates, by = "instanceid") %>%
  mutate(
    mainlevee = !is.na(date_bae),
    delay_depot_bae_min = as.numeric(difftime(date_bae, date_depot, units = "mins")),
    delay_pay_bae_days  = as.numeric(difftime(date_bae, date_payment, units = "days"))
  )

workflow_ops <- df_gen %>%
  count(op_name_std, sort = TRUE, name = "nb_operations")

rupture_ops <- workflow_ops %>%
  filter(op_name_std %in% c(
    "MODIFY_REGISTERED", "POST_ENTRY", "RE_ROUTE_QUERY", "RE_ROUTE_RED",
    "LOCK", "UNLOCK", "CANCEL"
  ))

workflow_ops
##                     op_name_std nb_operations
## 1                       PAYMENT        131331
## 2           VALIDATE_AND_ASSESS        130026
## 3                RE_ROUTE_GREEN        109339
## 4  ASSESS_FROM_SELECTED_STANDBY         98577
## 5           PRINT_RELEASE_ORDER         93635
## 6                  TOTALLY_EXIT         73780
## 7            REGISTER_T1_ON_SAD         32476
## 8                    POST_ENTRY          2799
## 9             MODIFY_REGISTERED          1331
## 10                       UNLOCK           929
## 11                VIEW_CRITERIA           472
## 12                         EXIT           367
## 13                         LOCK           140
## 14                    EXIT_PAID            23
## 15            ASSESS_REGISTERED            19
## 16         EXIT_ASSESSED_CREDIT            18
## 17               RE_ROUTE_QUERY             9
## 18                       REFUND             5
## 19   ASSESSED_DIRECT_TO_PREPAID             1
## 20                     VALIDATE             1
rupture_ops
##         op_name_std nb_operations
## 1        POST_ENTRY          2799
## 2 MODIFY_REGISTERED          1331
## 3            UNLOCK           929
## 4              LOCK           140
## 5    RE_ROUTE_QUERY             9

TODO étudiant :

  1. Expliquer pourquoi le workflow se calcule au grain déclaration et non au grain article.
  2. Parmi les opérations observées dans rupture_ops, lesquelles signalent une rupture de parcours ? Donner leur volume respectif et commenter l’absence éventuelle de certaines opérations attendues.

6 Comparaison initiale / finale au grain article

item_fl <- first_last_by_version(
  df_item,
  keys      = c("instanceid", "key_itm_nbr"),
  keep_cols = c("HS", "VIT_STV", "VIT_CIF", "gds_org_cty", "tar_prf")
)

item_compare <- item_fl$versions %>%
  left_join(
    item_fl$ini %>%
      rename(HS_ini = HS, VIT_STV_ini = VIT_STV, VIT_CIF_ini = VIT_CIF,
             ORG_ini = gds_org_cty, PRF_ini = tar_prf),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  left_join(
    item_fl$fin %>%
      rename(HS_fin = HS, VIT_STV_fin = VIT_STV, VIT_CIF_fin = VIT_CIF,
             ORG_fin = gds_org_cty, PRF_fin = tar_prf),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  mutate(
    chg_hs    = coalesce(HS_ini, "")  != coalesce(HS_fin, ""),
    chg_org   = coalesce(ORG_ini, "") != coalesce(ORG_fin, ""),
    chg_prf   = coalesce(PRF_ini, "") != coalesce(PRF_fin, ""),
    d_vit_stv = VIT_STV_fin - VIT_STV_ini,
    d_vit_cif = VIT_CIF_fin - VIT_CIF_ini
  )

item_presence <- item_fl$versions %>%
  left_join(
    item_fl$ini %>% transmute(instanceid, key_itm_nbr, in_ini = TRUE),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  left_join(
    item_fl$fin %>% transmute(instanceid, key_itm_nbr, in_fin = TRUE),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  mutate(
    in_ini = coalesce(in_ini, FALSE),
    in_fin = coalesce(in_fin, FALSE),
    item_status = case_when(
      in_ini & in_fin  ~ "PRESENT_DANS_LES_2",
      in_ini & !in_fin ~ "DISPARU_EN_FINAL",
      !in_ini & in_fin ~ "APPARU_EN_FINAL",
      TRUE             ~ "AUTRE"
    )
  )

item_presence_summary <- item_presence %>%
  count(item_status, name = "nb_items")

hs_change_vit_stable <- item_compare %>%
  filter(
    chg_hs == TRUE,
    coalesce(d_vit_stv, 0) == 0,
    coalesce(d_vit_cif, 0) == 0
  ) %>%
  select(instanceid, key_itm_nbr, HS_ini, HS_fin,
         VIT_STV_ini, VIT_STV_fin, VIT_CIF_ini, VIT_CIF_fin)

item_presence_summary
## # A tibble: 1 × 2
##   item_status        nb_items
##   <chr>                 <int>
## 1 PRESENT_DANS_LES_2   195898
head(hs_change_vit_stable, 20)
## # A tibble: 0 × 8
## # ℹ 8 variables: instanceid <int>, key_itm_nbr <int>, HS_ini <chr>,
## #   HS_fin <chr>, VIT_STV_ini <dbl>, VIT_STV_fin <dbl>, VIT_CIF_ini <dbl>,
## #   VIT_CIF_fin <dbl>

TODO étudiant :

  1. Interpréter les catégories de item_presence_summary : que signifient APPARU_EN_FINAL, DISPARU_EN_FINAL, PRESENT_DANS_LES_2 ? Que conclure si certaines catégories sont absentes du résultat ?
  2. Le tableau hs_change_vit_stable est-il vide ou non ? Que faut-il en conclure méthodologiquement — peut-on affirmer l’absence de tels cas ou l’absence de résultat est-elle contextuelle ?

7 Taxes : agrégation versionnée obligatoire

tax_item_ver_code <- df_tax %>%
  group_by(instanceid, key_itm_nbr, doc_ver, tax_lin_cod) %>%
  summarise(
    tax_lin_bse = sum(tax_lin_bse, na.rm = TRUE),
    tax_lin_amt = sum(tax_lin_amt, na.rm = TRUE),
    tax_lin_rat = max(tax_lin_rat, na.rm = TRUE),
    .groups = "drop"
  )

tax_item_ver <- tax_item_ver_code %>%
  group_by(instanceid, key_itm_nbr, doc_ver) %>%
  summarise(
    TAX         = sum(tax_lin_amt, na.rm = TRUE),
    n_tax_codes = n_distinct(tax_lin_cod),
    tax_codes   = paste(sort(unique(tax_lin_cod)), collapse = "|"),
    .groups     = "drop"
  )

rm(df_tax)
gc()
##            used  (Mb) gc trigger   (Mb)  max used   (Mb)
## Ncells  1808135  96.6   22541472 1203.9  35221048 1881.1
## Vcells 61176328 466.8  198378950 1513.6 169334386 1292.0
tax_fl <- first_last_by_version(
  tax_item_ver,
  keys      = c("instanceid", "key_itm_nbr"),
  keep_cols = c("TAX", "n_tax_codes", "tax_codes")
)

item_tax_compare <- tax_fl$versions %>%
  left_join(
    tax_fl$ini %>%
      rename(TAX_INI = TAX, N_TAX_INI = n_tax_codes, TAX_CODES_INI = tax_codes),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  left_join(
    tax_fl$fin %>%
      rename(TAX_FIN = TAX, N_TAX_FIN = n_tax_codes, TAX_CODES_FIN = tax_codes),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  mutate(
    DELTA_TAX     = TAX_FIN - TAX_INI,
    ABS_DELTA_TAX = abs(DELTA_TAX),
    chg_tax_codes = coalesce(TAX_CODES_INI, "") != coalesce(TAX_CODES_FIN, "")
  )

delta_tax_hs_stable <- item_compare %>%
  left_join(
    item_tax_compare,
    by = c("instanceid", "key_itm_nbr", "DOC_VER_MIN", "DOC_VER_MAX", "nb_versions")
  ) %>%
  filter(chg_hs == FALSE, coalesce(ABS_DELTA_TAX, 0) > 0) %>%
  select(instanceid, key_itm_nbr, HS_ini, HS_fin, TAX_INI, TAX_FIN, DELTA_TAX, ABS_DELTA_TAX)

tax_code_changes <- item_tax_compare %>%
  filter(chg_tax_codes == TRUE) %>%
  select(instanceid, key_itm_nbr, TAX_CODES_INI, TAX_CODES_FIN, TAX_INI, TAX_FIN, DELTA_TAX)

head(delta_tax_hs_stable, 20)
## # A tibble: 20 × 8
##    instanceid key_itm_nbr HS_ini  HS_fin TAX_INI TAX_FIN DELTA_TAX ABS_DELTA_TAX
##         <int>       <int> <chr>   <chr>    <dbl>   <dbl>     <dbl>         <dbl>
##  1    2682538           1 392390… 39239…  1.85e5  2.80e5    95207.        95207.
##  2    2682752           1 390290… 39029…  9.76e5  1.28e6   299224.       299224.
##  3    2682875           1 340249… 34024…  1.76e6  2.47e6   709008.       709008.
##  4    2682956           1 851640… 85164…  2.87e5  5.41e5   253394.       253394.
##  5    2682956           2 851610… 85161…  2.48e5  5.13e5   265101.       265101.
##  6    2682956           3 851660… 85166…  4.63e5  7.31e5   268346.       268346.
##  7    2682956           6 841840… 84184…  3.71e6  3.97e6   261924.       261924.
##  8    2682969           3 854449… 85444…  6.21e5  8.81e5   260661.       260661.
##  9    2684539           1 040221… 04022…  9.45e6  1.50e7  5512386.      5512386.
## 10    2684605           1 040221… 04022…  1.10e7  1.55e7  4547719.      4547719.
## 11    2684615           1 870422… 87042…  5.94e5  5.32e5   -61221.        61221.
## 12    2684630           1 870210… 87021…  2.68e5  4.28e5   160583.       160583.
## 13    2684903           1 870422… 87042…  5.18e5  6.17e5    99277.        99277.
## 14    2685034           1 870421… 87042…  4.03e5  3.39e5   -64882.        64882.
## 15    2685195           1 550810… 55081…  2.91e6  5.88e6  2968668.      2968668.
## 16    2685502           1 870421… 87042…  3.39e5  4.03e5    64882.        64882.
## 17    2685679           1 391890… 39189…  4.13e6  5.43e6  1303399.      1303399.
## 18    2685742           1 391890… 39189…  4.13e6  5.43e6  1303666.      1303666.
## 19    2686135           1 200899… 20089…  1.71e5  3.21e5   150068.       150068.
## 20    2686135           2 640220… 64022…  1.14e5  1.10e5    -4356.         4356.
head(tax_code_changes, 20)
## # A tibble: 20 × 7
##    instanceid key_itm_nbr TAX_CODES_INI  TAX_CODES_FIN TAX_INI TAX_FIN DELTA_TAX
##         <int>       <int> <chr>          <chr>           <dbl>   <dbl>     <dbl>
##  1    2684615           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  5.94e5  5.32e5   -61221.
##  2    2684903           1 DD|PC|PCS|PUA… DD|ISB|PC|PC…  5.18e5  6.17e5    99277.
##  3    2685034           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  4.03e5  3.39e5   -64882.
##  4    2685502           1 DD|PC|PCS|PUA… DD|ISB|PC|PC…  3.39e5  4.03e5    64882.
##  5    2686798           1 DD|PC|PCS|PUA… ADA|DD|PC|PC…  2.44e6  3.04e6   601128.
##  6    2687817           1 DD|PC|PCS|PUA… DD|ISB|PC|PC…  4.06e5  4.84e5    77857.
##  7    2688654           1 ADA|DD|PC|PCS… DD|PC|PCS|PU…  1.58e6  1.27e6  -312097.
##  8    2688925           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  1.01e5  2.82e5   181221.
##  9    2691103           1 DD|PC|PCS|PUA… ADA|DD|PC|PC…  4.42e5  5.51e5   108846.
## 10    2691916           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  1.00e6  8.85e5  -117178.
## 11    2692785           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  2.01e6  3.59e5 -1648451.
## 12    2692787           1 DD|ISB|PC|PCS… DD|PC|PCS|PU…  2.27e6  6.25e5 -1648632.
## 13    2693156           1 TSR            ISB|TSR        2.47e4  3.46e4     9895.
## 14    2695188           1 DD|PC|PCS|RSI… DD|ISB|PC|PC…  6.45e5  6.45e5      129.
## 15    2696851           1 ADA|DD|PC|PCS… DD|PC|PCS|PU…  6.49e5  3.90e5  -258366.
## 16    2697032           1 TVA            DD|RSI|TVA|T…  5.59e5  1.32e6   764346.
## 17    2697053           1 PCS|TVA|TVI    DD|PCS|RSI|T…  9.58e5  2.10e6  1143171.
## 18    2697069           1 PCS|TVA|TVI    DD|PCS|RSI|T…  9.58e5  2.10e6  1143171.
## 19    2697081           1 PCS|TVA|TVI    DD|PCS|RSI|T…  9.58e5  2.10e6  1143171.
## 20    2697119           1 DD|ISB|PCS|PI… DD|PCS|RSI|T…  2.52e5  2.95e4  -222214.

TODO étudiant :

  1. Expliquer pourquoi l’agrégation par tax_lin_cod est réalisée avant la comparaison initiale/finale. Que se passerait-il si on comparait directement les lignes brutes de df_tax ?
  2. Que signifie un DELTA_TAX non nul pour un article dont le HS n’a pas changé ? Donner 2 exemples de causes possibles.
  3. Que révèle un changement de tax_codes (ex. apparition ou disparition du code ISB ou ADA) entre version initiale et finale ?

8 Table analytique article

header_ref <- df_gen %>%
  group_by(instanceid) %>%
  arrange(doc_ver, op_date_time, .by_group = TRUE) %>%
  slice_tail(n = 1) %>%
  ungroup() %>%
  select(instanceid, cmp_con_cod, cmp_exp_cod, cmp_fis_cod,
         ide_cuo_cod, ide_cuo_nam, owner, exa_exa)

item_compare_full <- item_compare %>%
  left_join(
    item_tax_compare %>%
      select(instanceid, key_itm_nbr, TAX_INI, TAX_FIN, DELTA_TAX, ABS_DELTA_TAX, chg_tax_codes),
    by = c("instanceid", "key_itm_nbr")
  ) %>%
  left_join(header_ref, by = "instanceid") %>%
  mutate(
    FLAG_BAISSE_TAX = DELTA_TAX < 0,
    SENS_DELTA_TAX  = case_when(
      DELTA_TAX < 0 ~ "BAISSE_TAX",
      DELTA_TAX > 0 ~ "HAUSSE_TAX",
      TRUE          ~ "STABLE"
    )
  )

TODO étudiant :

  1. Pourquoi item_compare_full est-elle construite au grain article et non au grain déclaration ? Quels types de jointures permettent de l’enrichir avec les informations d’en-tête (header_ref) ?
  2. Pourquoi header_ref est-il construit en prenant la dernière ligne par instanceid (tri par doc_ver + op_date_time, slice_tail) plutôt que n’importe quelle ligne ?

9 Workflow normal et seuil Q95_workflow

decl_stability <- item_compare_full %>%
  group_by(instanceid) %>%
  summarise(
    hs_stable_decl  = all(!chg_hs, na.rm = TRUE),
    vit_stable_decl = all(coalesce(d_vit_stv, 0) == 0, na.rm = TRUE),
    tax_stable_decl = all(coalesce(DELTA_TAX, 0) == 0, na.rm = TRUE),
    stable_decl     = hs_stable_decl & vit_stable_decl & tax_stable_decl,
    .groups = "drop"
  )

workflow_ref <- decl_workflow %>%
  left_join(decl_stability, by = "instanceid") %>%
  filter(mainlevee == TRUE, stable_decl == TRUE)

Q95_workflow <- workflow_ref %>%
  summarise(Q95 = quantile(nb_versions, 0.95, na.rm = TRUE)) %>%
  pull(Q95)

decl_workflow <- decl_workflow %>%
  left_join(decl_stability, by = "instanceid") %>%
  mutate(cascade = nb_versions > Q95_workflow)

Q95_workflow
## 95% 
##   7

TODO étudiant :

  1. Le workflow normal est défini par 4 conditions cumulatives. Expliquer le rôle de chacune (mainlevee, hs_stable, vit_stable, tax_stable) et justifier pourquoi elles doivent être vérifiées ensemble.
  2. Quelle est la valeur de Q95_workflow obtenue ? Interpréter ce seuil : qu’est-ce qu’il représente pour la population de référence ?
  3. Pourquoi utilise-t-on le P95 plutôt que, par exemple, la moyenne ou le P99 comme seuil de détection des cascades ?

10 Seuil normal de DELTA_TAX

normal_tax <- item_compare_full %>%
  inner_join(workflow_ref %>% select(instanceid), by = "instanceid") %>%
  filter(chg_hs == FALSE)

tax_threshold <- normal_tax %>%
  summarise(
    N_REF   = n(),
    P90_TAX = quantile(ABS_DELTA_TAX, 0.90, na.rm = TRUE),
    P95_TAX = quantile(ABS_DELTA_TAX, 0.95, na.rm = TRUE),
    P99_TAX = quantile(ABS_DELTA_TAX, 0.99, na.rm = TRUE)
  )

eps_tax <- tax_threshold$P95_TAX[[1]]

tax_threshold
## # A tibble: 1 × 4
##    N_REF P90_TAX P95_TAX P99_TAX
##    <int>   <dbl>   <dbl>   <dbl>
## 1 131179       0       0       0
cat("eps_tax retenu (P95) =", format(round(eps_tax, 2), big.mark = " "), "\n")
## eps_tax retenu (P95) = 0
item_tax_flag <- item_compare_full %>%
  mutate(
    FLAG_ATYPIQUE_TAX = ABS_DELTA_TAX > eps_tax,
    FLAG_BAISSE_TAX   = DELTA_TAX < 0,
    SENS_DELTA_TAX    = case_when(
      DELTA_TAX < 0 ~ "BAISSE_TAX",
      DELTA_TAX > 0 ~ "HAUSSE_TAX",
      TRUE          ~ "STABLE"
    )
  ) %>%
  select(instanceid, key_itm_nbr, HS_ini, HS_fin, TAX_INI, TAX_FIN,
         DELTA_TAX, ABS_DELTA_TAX, SENS_DELTA_TAX, FLAG_BAISSE_TAX, FLAG_ATYPIQUE_TAX)

head(item_tax_flag, 20)
## # A tibble: 20 × 11
##    instanceid key_itm_nbr HS_ini  HS_fin TAX_INI TAX_FIN DELTA_TAX ABS_DELTA_TAX
##         <int>       <int> <chr>   <chr>    <dbl>   <dbl>     <dbl>         <dbl>
##  1    2682290           1 870322… 87032…  3.90e5  3.90e5         0             0
##  2    2682293           1 870322… 87032…  3.90e5  3.90e5         0             0
##  3    2682296           1 190219… 19021…  2.59e5  2.59e5         0             0
##  4    2682296           2 110100… 11010…  4.42e5  4.42e5         0             0
##  5    2682296           3 170199… 17019…  2.07e5  2.07e5         0             0
##  6    2682298           1 190219… 19021…  2.62e5  2.62e5         0             0
##  7    2682298           2 110100… 11010…  4.47e5  4.47e5         0             0
##  8    2682298           3 080410… 08041…  3.65e4  3.65e4         0             0
##  9    2682298           4 200990… 20099…  1.19e5  1.19e5         0             0
## 10    2682298           5 040221… 04022…  6.39e4  6.39e4         0             0
## 11    2682301           1 870899… 87089… NA      NA             NA            NA
## 12    2682304           1 870322… 87032…  3.59e5  3.59e5         0             0
## 13    2682305           1 330499… 33049…  1.91e5  1.91e5         0             0
## 14    2682305           2 690740… 69074…  6.50e4  6.50e4         0             0
## 15    2682305           3 841381… 84138…  1.35e5  1.35e5         0             0
## 16    2682305           4 401110… 40111…  6.30e4  6.30e4         0             0
## 17    2682306           1 100620… 10062…  2.07e6  2.07e6         0             0
## 18    2682307           1 100620… 10062…  1.72e6  1.72e6         0             0
## 19    2682308           1 870322… 87032…  3.34e4  3.34e4         0             0
## 20    2682309           1 902789… 90278…  0       0              0             0
## # ℹ 3 more variables: SENS_DELTA_TAX <chr>, FLAG_BAISSE_TAX <lgl>,
## #   FLAG_ATYPIQUE_TAX <lgl>

TODO étudiant :

  1. Sur quelle population de référence est calculé eps_tax ? Pourquoi est-il important de se restreindre aux articles en workflow normal avec chg_hs == FALSE ?
  2. La valeur obtenue pour eps_tax est-elle attendue ? Si eps_tax = 0, que cela implique-t-il pour la condition FLAG_ATYPIQUE_TAX = ABS_DELTA_TAX > eps_tax ? Comment faut-il lire le scoring de risque dans ce cas ?
  3. Comparer P90, P95 et P99 : quel seuil choisiriez-vous pour le ciblage opérationnel et pourquoi ?

11 Règles interprétables de niveau de risque

item_compare_full <- item_compare_full %>%
  mutate(
    risk_level = case_when(
      chg_hs & ABS_DELTA_TAX > eps_tax                                                        ~ "FORT",
      !chg_hs & ABS_DELTA_TAX > eps_tax                                                       ~ "FORT",
      chg_hs & ABS_DELTA_TAX == 0                                                             ~ "MOYEN",
      chg_hs & ABS_DELTA_TAX > 0 & ABS_DELTA_TAX <= eps_tax                                  ~ "MOYEN",
      (chg_org | chg_prf) & ABS_DELTA_TAX <= eps_tax                                          ~ "MOYEN",
      ABS_DELTA_TAX == 0 & !chg_hs & !chg_org & !chg_prf &
        abs(coalesce(d_vit_stv, 0)) == 0 & abs(coalesce(d_vit_cif, 0)) == 0                  ~ "AUCUN",
      TRUE                                                                                     ~ "FAIBLE"
    )
  )

org_prf_cases <- item_compare_full %>%
  filter(chg_org | chg_prf) %>%
  count(chg_org, chg_prf, risk_level, sort = TRUE, name = "nb_items")

risk_distribution <- item_compare_full %>%
  count(risk_level, sort = TRUE, name = "nb_items") %>%
  mutate(part = nb_items / sum(nb_items))

org_prf_cases
## # A tibble: 5 × 4
##   chg_org chg_prf risk_level nb_items
##   <lgl>   <lgl>   <chr>         <int>
## 1 TRUE    FALSE   FORT             33
## 2 TRUE    FALSE   MOYEN            29
## 3 TRUE    FALSE   FAIBLE           15
## 4 FALSE   TRUE    FORT              8
## 5 TRUE    TRUE    FORT              1
risk_distribution
## # A tibble: 4 × 3
##   risk_level nb_items     part
##   <chr>         <int>    <dbl>
## 1 AUCUN        149868 0.765   
## 2 FAIBLE        44628 0.228   
## 3 FORT           1265 0.00646 
## 4 MOYEN           137 0.000699

TODO étudiant :

  1. Décrire la logique de la matrice de risque : pourquoi un article avec !chg_hs mais ABS_DELTA_TAX > eps_tax est-il classé FORT et non FAIBLE ?
  2. Pourquoi les changements d’ORG ou PRF sans impact fiscal sont-ils classés MOYEN et non FAIBLE ?
  3. Commenter la distribution observée dans risk_distribution. Est-elle cohérente avec la nature du système douanier analysé ?
  4. Si eps_tax = 0, quel effet cela a-t-il sur les catégories MOYEN et AUCUN ? Comment recalibrer la règle si ce cas se présente ?

12 Questions du cas pratique

Pour chaque question : 1. produire le tableau demandé ; 2. trier du plus important au moins important ; 3. rédiger une interprétation courte en 2-3 phrases maximum.

12.1 Q1 — Identifier les 5 importateurs les plus redressés

Objectif métier : classer les importateurs selon le nombre de déclarations redressées et le montant total de redressement.

Sortie attendue : tableau avec cmp_con_cod, nb_declarations_redressees, montant_total_redressement.

q1 <- item_compare_full %>%
  filter(risk_level == "FORT", DELTA_TAX > 0) %>%
  group_by(cmp_con_cod) %>%
  summarise(
    nb_declarations_redressees  = n_distinct(instanceid),
    montant_total_redressement  = sum(DELTA_TAX, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(montant_total_redressement), desc(nb_declarations_redressees))

q1_concentration <- q1 %>%
  mutate(
    part_montant = montant_total_redressement / sum(montant_total_redressement, na.rm = TRUE),
    cumul_part   = cumsum(part_montant)
  )

head(q1, 5)
## # A tibble: 5 × 3
##   cmp_con_cod  nb_declarations_redressees montant_total_redressement
##   <chr>                             <int>                      <dbl>
## 1 COD_65739828                        110                  54700313.
## 2 COD_BF47887D                         15                  43514924.
## 3 COD_19F52A62                          1                  34726185.
## 4 COD_D23C1BA1                         33                  33677629.
## 5 COD_95C46A9C                          2                  25741305.
head(q1_concentration, 10)
## # A tibble: 10 × 5
##    cmp_con_cod  nb_declarations_redressees montant_total_redresse…¹ part_montant
##    <chr>                             <int>                    <dbl>        <dbl>
##  1 COD_65739828                        110                54700313.       0.105 
##  2 COD_BF47887D                         15                43514924.       0.0835
##  3 COD_19F52A62                          1                34726185.       0.0666
##  4 COD_D23C1BA1                         33                33677629.       0.0646
##  5 COD_95C46A9C                          2                25741305.       0.0494
##  6 COD_C6B55A2A                         37                21206933.       0.0407
##  7 COD_04D7E70E                         33                19644107.       0.0377
##  8 COD_BA7FDA7A                          2                18425042.       0.0354
##  9 COD_951EF987                          6                16835605.       0.0323
## 10 COD_B994AC9D                         32                16372078.       0.0314
## # ℹ abbreviated name: ¹​montant_total_redressement
## # ℹ 1 more variable: cumul_part <dbl>

TODO étudiant : Interpréter les 5 premiers importateurs. Quelle est la part cumulée des 5 premiers dans le montant total de redressement ? Que révèle cette concentration sur la structure du risque fiscal ?


12.2 Q2 — Repérer les déclarations avec DEPOT → BAE < 120 min

Objectif métier : repérer les déclarations très rapides (délai dépôt–mainlevée inférieur à 2 heures), puis préciser qui, combien et dans quels bureaux.

Sortie attendue : tableau détaillé avec instanceid, cmp_con_cod, ide_cuo_cod, ide_cuo_nam, date_depot, date_bae, delay_depot_bae_min ; synthèse par bureau.

q2_detail <- decl_workflow %>%
  left_join(
    header_ref %>% select(instanceid, cmp_con_cod, ide_cuo_cod, ide_cuo_nam, owner),
    by = "instanceid"
  ) %>%
  filter(!is.na(delay_depot_bae_min), delay_depot_bae_min < 120) %>%
  arrange(delay_depot_bae_min) %>%
  select(instanceid, cmp_con_cod, ide_cuo_cod, ide_cuo_nam, owner,
         date_depot, date_bae, delay_depot_bae_min)

q2_bureau <- q2_detail %>%
  group_by(ide_cuo_cod, ide_cuo_nam) %>%
  summarise(nb_cas = n(), .groups = "drop") %>%
  arrange(desc(nb_cas))

head(q2_detail, 20)
## # A tibble: 0 × 8
## # ℹ 8 variables: instanceid <int>, cmp_con_cod <chr>, ide_cuo_cod <chr>,
## #   ide_cuo_nam <chr>, owner <chr>, date_depot <dttm>, date_bae <dttm>,
## #   delay_depot_bae_min <dbl>
q2_bureau
## # A tibble: 0 × 3
## # ℹ 3 variables: ide_cuo_cod <chr>, ide_cuo_nam <chr>, nb_cas <int>

TODO étudiant :

  1. Le tableau q2_detail est-il vide ? Si oui, proposer une interprétation méthodologique : est-ce dû au seuil choisi, au proxy utilisé pour date_bae, ou à la nature réelle des données ?
  2. Si le résultat est vide, proposer deux pistes d’amélioration du proxy ou du seuil pour rendre cette détection opérationnelle.

12.3 Q3 — Analyser les opérations hors heures ouvrables ou pendant le week-end

Objectif métier : identifier les bureaux et agents (owner) les plus concernés.

Note : owner est utilisé ici comme code déclarant. end_user n’est pas analysé dans cet atelier.

Sortie attendue : deux tableaux, l’un pour les opérations hors heures (avant 8h ou à partir de 18h), l’autre pour les opérations du week-end.

ops_time <- df_gen %>%
  mutate(
    heure    = lubridate::hour(op_date_time),
    jour_num = lubridate::wday(op_date_time, week_start = 1),
    jour     = c("Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim")[jour_num],
    hors_heures = !is.na(heure) & (heure < 8 | heure >= 18),
    weekend     = !is.na(jour_num) & jour_num %in% c(6, 7)
  )

q3_hors_heures <- ops_time %>%
  filter(hors_heures) %>%
  group_by(ide_cuo_cod, ide_cuo_nam, owner) %>%
  summarise(nb_operations_hors_heures = n(), .groups = "drop") %>%
  arrange(desc(nb_operations_hors_heures))

q3_weekend <- ops_time %>%
  filter(weekend) %>%
  group_by(ide_cuo_cod, ide_cuo_nam, owner) %>%
  summarise(nb_operations_weekend = n(), .groups = "drop") %>%
  arrange(desc(nb_operations_weekend))

head(q3_hors_heures, 20)
## # A tibble: 20 × 4
##    ide_cuo_cod  ide_cuo_nam  owner        nb_operations_hors_heures
##    <chr>        <chr>        <chr>                            <int>
##  1 CUO_8062B850 CUO_499564F0 COD_10189223                      9978
##  2 CUO_313A6A2E CUO_CA6A5B1B COD_6CFDEE48                      4961
##  3 CUO_88C4B39F CUO_7145CEDD COD_819CF064                      4105
##  4 CUO_313A6A2E CUO_CA6A5B1B COD_1D44BB6E                      3979
##  5 CUO_61E48A76 CUO_83E92C90 COD_DC31D3E2                      3820
##  6 CUO_8062B850 CUO_499564F0 COD_F22F246E                      3745
##  7 CUO_6BC153ED CUO_DFC2161D COD_6CFDEE48                      3386
##  8 CUO_6BC153ED CUO_DFC2161D COD_819CF064                      3286
##  9 CUO_313A6A2E CUO_CA6A5B1B COD_91A0D8A8                      2808
## 10 CUO_88C4B39F CUO_7145CEDD COD_2A7D61D5                      2726
## 11 CUO_08824E78 CUO_B80496FD COD_3382CCAB                      2611
## 12 CUO_8062B850 CUO_499564F0 COD_29D2B98B                      2529
## 13 CUO_8062B850 CUO_499564F0 COD_5F795437                      2516
## 14 CUO_88C4B39F CUO_7145CEDD COD_6AE100D0                      2442
## 15 CUO_313A6A2E CUO_CA6A5B1B COD_7E58B39D                      2417
## 16 CUO_57FDEDA7 CUO_F674DC97 COD_9CA665A8                      2352
## 17 CUO_6BC153ED CUO_DFC2161D COD_29D2B98B                      2277
## 18 CUO_313A6A2E CUO_CA6A5B1B COD_347779D3                      2166
## 19 CUO_E01087D7 CUO_3355DC7F COD_1D44BB6E                      2147
## 20 CUO_313A6A2E CUO_CA6A5B1B COD_B51ECEFB                      2111
head(q3_weekend, 20)
## # A tibble: 20 × 4
##    ide_cuo_cod  ide_cuo_nam  owner        nb_operations_weekend
##    <chr>        <chr>        <chr>                        <int>
##  1 CUO_8062B850 CUO_499564F0 COD_10189223                  6124
##  2 CUO_61E48A76 CUO_83E92C90 COD_DC31D3E2                  3896
##  3 CUO_313A6A2E CUO_CA6A5B1B COD_6CFDEE48                  3413
##  4 CUO_E01087D7 CUO_3355DC7F COD_10189223                  3351
##  5 CUO_E01087D7 CUO_3355DC7F COD_1D44BB6E                  3055
##  6 CUO_88C4B39F CUO_7145CEDD COD_819CF064                  2825
##  7 CUO_313A6A2E CUO_CA6A5B1B COD_1D44BB6E                  2581
##  8 CUO_57FDEDA7 CUO_F674DC97 COD_9CA665A8                  2550
##  9 CUO_250974A4 CUO_C99A3DF6 COD_5D5A6B83                  2340
## 10 CUO_88C4B39F CUO_7145CEDD COD_2A7D61D5                  2149
## 11 CUO_313A6A2E CUO_CA6A5B1B COD_7E58B39D                  2130
## 12 CUO_D08406A2 CUO_3061FD94 COD_BA899B2D                  2128
## 13 CUO_313A6A2E CUO_CA6A5B1B COD_29D2B98B                  2053
## 14 CUO_6BC153ED CUO_DFC2161D COD_819CF064                  1981
## 15 CUO_FA720F78 CUO_14AB4E59 COD_91A0D8A8                  1961
## 16 CUO_D08406A2 CUO_3061FD94 COD_6CFDEE48                  1960
## 17 CUO_313A6A2E CUO_CA6A5B1B COD_347779D3                  1842
## 18 CUO_313A6A2E CUO_CA6A5B1B COD_91A0D8A8                  1822
## 19 CUO_8062B850 CUO_499564F0 COD_F22F246E                  1816
## 20 CUO_6BC153ED CUO_DFC2161D COD_6CFDEE48                  1792

TODO étudiant : Identifier les bureaux les plus concernés par les opérations hors heures et le week-end. Un même bureau ou déclarant apparaît-il dans les deux tableaux ? Que cela suggère-t-il ? Rappeler pourquoi il s’agit d’un signal de process et non d’une conclusion individuelle directe.


12.4 Q4 — Identifier les déclarations avec nb_versions > Q95_workflow

Objectif métier : repérer les déclarations avec un nombre de versions atypique, ventiler par bureau, et croiser avec les redressements associés.

Sortie attendue : tableau avec instanceid, nb_versions, Q95_workflow, surplus_versions, ide_cuo_cod, ide_cuo_nam, owner, exa_exa ; ventilation par bureau ; extension avec les montants de redressement.

q4 <- decl_workflow %>%
  left_join(
    header_ref %>% select(instanceid, ide_cuo_cod, ide_cuo_nam, owner, exa_exa),
    by = "instanceid"
  ) %>%
  filter(cascade == TRUE) %>%
  mutate(
    Q95_workflow     = Q95_workflow,
    surplus_versions = nb_versions - Q95_workflow
  ) %>%
  arrange(desc(nb_versions))

head(q4, 20)
## # A tibble: 20 × 22
##    instanceid DOC_VER_MIN DOC_VER_MAX nb_versions date_depot         
##         <int>       <int>       <int>       <int> <dttm>             
##  1    2741376           1          51          51 2022-04-02 02:00:00
##  2    2831489           1          43          43 2022-08-31 02:00:00
##  3    2744878           1          39          39 2022-04-12 02:00:00
##  4    2690518           1          36          36 2022-01-11 01:00:00
##  5    2726190           1          35          35 2022-02-23 01:00:00
##  6    2832726           1          34          34 2022-09-02 02:00:00
##  7    2708054           1          27          27 2022-02-01 01:00:00
##  8    2723536           1          27          27 2022-02-19 01:00:00
##  9    2691868           1          26          26 2022-01-12 01:00:00
## 10    2715681           1          25          25 2022-02-09 01:00:00
## 11    2835689           1          25          25 2022-09-07 02:00:00
## 12    2846177           1          25          25 2022-09-22 02:00:00
## 13    2692683           1          24          24 2022-01-13 01:00:00
## 14    2695164           1          24          24 2022-01-15 01:00:00
## 15    2695176           1          23          23 2022-01-15 01:00:00
## 16    2715338           1          23          23 2022-02-09 01:00:00
## 17    2726872           1          23          23 2022-02-23 01:00:00
## 18    2808465           1          22          22 2022-07-27 02:00:00
## 19    2689673           1          21          21 2022-01-09 01:00:00
## 20    2712320           1          21          21 2022-02-05 01:00:00
## # ℹ 17 more variables: date_bae <dttm>, date_payment <dttm>, op_last <dttm>,
## #   mainlevee <lgl>, delay_depot_bae_min <dbl>, delay_pay_bae_days <dbl>,
## #   hs_stable_decl <lgl>, vit_stable_decl <lgl>, tax_stable_decl <lgl>,
## #   stable_decl <lgl>, cascade <lgl>, ide_cuo_cod <chr>, ide_cuo_nam <chr>,
## #   owner <chr>, exa_exa <chr>, Q95_workflow <dbl>, surplus_versions <dbl>
# Ventilation par bureau
q4_bureau <- q4 %>%
  count(ide_cuo_cod, ide_cuo_nam, sort = TRUE, name = "nb_declarations_cascade")

q4_bureau
## # A tibble: 19 × 3
##    ide_cuo_cod  ide_cuo_nam  nb_declarations_cascade
##    <chr>        <chr>                          <int>
##  1 CUO_313A6A2E CUO_CA6A5B1B                    1020
##  2 CUO_6BC153ED CUO_DFC2161D                     583
##  3 CUO_FA720F78 CUO_14AB4E59                     495
##  4 CUO_D08406A2 CUO_3061FD94                     259
##  5 CUO_BA4A1571 CUO_54D892CD                     249
##  6 CUO_8D6D6EF8 CUO_014E9359                     228
##  7 CUO_8062B850 CUO_499564F0                     213
##  8 CUO_537C11F3 CUO_6656FDFD                     151
##  9 CUO_E01087D7 CUO_3355DC7F                     134
## 10 CUO_57FDEDA7 CUO_F674DC97                     132
## 11 CUO_61E48A76 CUO_83E92C90                     112
## 12 CUO_8F4D3372 CUO_BBFD7BA3                      86
## 13 CUO_A330C94C CUO_B9BD7F9F                      55
## 14 CUO_A4FFC1CE CUO_8AC30101                      36
## 15 CUO_250974A4 CUO_C99A3DF6                      18
## 16 CUO_9294067C CUO_5A8393D7                       9
## 17 CUO_2EF99218 CUO_83A56DC6                       1
## 18 CUO_6BC153ED CUO_5EC09149                       1
## 19 CUO_AE023546 CUO_7D2DB62F                       1
# Extension : croisement cascade × redressements
q4_redressement <- q4 %>%
  left_join(
    item_compare_full %>%
      filter(risk_level == "FORT", DELTA_TAX > 0) %>%
      group_by(instanceid) %>%
      summarise(
        montant_redressement  = sum(DELTA_TAX, na.rm = TRUE),
        nb_articles_redresses = n(),
        .groups = "drop"
      ),
    by = "instanceid"
  ) %>%
  mutate(
    redresse             = !is.na(montant_redressement),
    montant_redressement = coalesce(montant_redressement, 0)
  )

cat("Déclarations en cascade          :", nrow(q4), "\n")
## Déclarations en cascade          : 3783
cat("Dont redressées (FORT + hausse)  :", sum(q4_redressement$redresse), "\n")
## Dont redressées (FORT + hausse)  : 368
cat("Taux de redressement             :", round(mean(q4_redressement$redresse) * 100, 1), "%\n")
## Taux de redressement             : 9.7 %
cat("Montant total redressement cascade:", format(sum(q4_redressement$montant_redressement), big.mark = " "), "\n")
## Montant total redressement cascade: 221 886 084
q4_bureau_redressement <- q4_redressement %>%
  group_by(ide_cuo_cod, ide_cuo_nam) %>%
  summarise(
    nb_declarations_cascade = n(),
    nb_redressees           = sum(redresse),
    taux_redressement       = round(mean(redresse) * 100, 1),
    montant_total_redress   = sum(montant_redressement, na.rm = TRUE),
    nb_versions_max         = max(nb_versions),
    .groups = "drop"
  ) %>%
  arrange(desc(montant_total_redress))

q4_bureau_redressement
## # A tibble: 19 × 7
##    ide_cuo_cod  ide_cuo_nam  nb_declarations_cascade nb_redressees
##    <chr>        <chr>                          <int>         <int>
##  1 CUO_6BC153ED CUO_DFC2161D                     583            48
##  2 CUO_D08406A2 CUO_3061FD94                     259            46
##  3 CUO_FA720F78 CUO_14AB4E59                     495           126
##  4 CUO_8D6D6EF8 CUO_014E9359                     228            25
##  5 CUO_BA4A1571 CUO_54D892CD                     249            22
##  6 CUO_537C11F3 CUO_6656FDFD                     151            14
##  7 CUO_E01087D7 CUO_3355DC7F                     134            17
##  8 CUO_61E48A76 CUO_83E92C90                     112            14
##  9 CUO_250974A4 CUO_C99A3DF6                      18             2
## 10 CUO_A4FFC1CE CUO_8AC30101                      36             5
## 11 CUO_57FDEDA7 CUO_F674DC97                     132            10
## 12 CUO_A330C94C CUO_B9BD7F9F                      55             4
## 13 CUO_8062B850 CUO_499564F0                     213             8
## 14 CUO_8F4D3372 CUO_BBFD7BA3                      86            13
## 15 CUO_313A6A2E CUO_CA6A5B1B                    1020            14
## 16 CUO_2EF99218 CUO_83A56DC6                       1             0
## 17 CUO_6BC153ED CUO_5EC09149                       1             0
## 18 CUO_9294067C CUO_5A8393D7                       9             0
## 19 CUO_AE023546 CUO_7D2DB62F                       1             0
## # ℹ 3 more variables: taux_redressement <dbl>, montant_total_redress <dbl>,
## #   nb_versions_max <int>

TODO étudiant :

  1. Commenter le lien entre multiplication des versions et rupture de workflow : une cascade documentaire implique-t-elle systématiquement un redressement fiscal ?
  2. À partir de q4_bureau_redressement, identifier les bureaux où la cascade est la plus fortement associée à un redressement effectif (taux et montant). Distinguer les bureaux à fort volume de cascade sans redressement notable de ceux à cascade + impact fiscal fort.
  3. Que pourrait signifier un bureau avec beaucoup de cascades mais peu de redressements ? Et inversement ?

12.5 Q5 — Repérer les articles avec HS modifié et qualifier l’effet fiscal

Objectif métier : analyser les articles dont le code HS a été modifié entre version initiale et version finale, puis qualifier l’effet fiscal associé.

Sortie attendue : tableau au grain article avec instanceid, key_itm_nbr, HS_ini, HS_fin, TAX_INI, TAX_FIN, DELTA_TAX, ABS_DELTA_TAX, risk_level.

q5 <- item_compare_full %>%
  filter(chg_hs == TRUE) %>%
  arrange(desc(ABS_DELTA_TAX)) %>%
  select(instanceid, key_itm_nbr, HS_ini, HS_fin, TAX_INI, TAX_FIN,
         DELTA_TAX, ABS_DELTA_TAX, SENS_DELTA_TAX, FLAG_BAISSE_TAX, risk_level)

head(q5, 30)
## # A tibble: 30 × 11
##    instanceid key_itm_nbr HS_ini  HS_fin TAX_INI TAX_FIN DELTA_TAX ABS_DELTA_TAX
##         <int>       <int> <chr>   <chr>    <dbl>   <dbl>     <dbl>         <dbl>
##  1    2909136           1 271019… 27101…  2.22e6  1.51e7 12870653.     12870653.
##  2    2909140           1 271019… 27101…  2.22e6  1.51e7 12870653.     12870653.
##  3    2717101           1 190190… 19019…  4.64e6  1.05e7  5862754.      5862754.
##  4    2717135           1 190190… 19019…  4.64e6  1.05e7  5862754.      5862754.
##  5    2717144           1 190190… 19019…  4.64e6  1.05e7  5862754.      5862754.
##  6    2856336           1 851769… 85366…  1.53e7  2.06e7  5395701.      5395701.
##  7    2682561           1 190190… 04021…  6.34e6  1.11e7  4748598.      4748598.
##  8    2805765           1 870840… 87021…  7.32e5  5.10e6  4371452.      4371452.
##  9    2805761           1 870840… 87021…  7.67e5  4.50e6  3729116.      3729116.
## 10    2853086           1 170199… 17019…  3.06e6  6.71e6  3651098.      3651098.
## # ℹ 20 more rows
## # ℹ 3 more variables: SENS_DELTA_TAX <chr>, FLAG_BAISSE_TAX <lgl>,
## #   risk_level <chr>
q5_baisse_tax <- item_compare_full %>%
  filter(chg_hs == TRUE, DELTA_TAX < 0) %>%
  arrange(DELTA_TAX) %>%
  select(instanceid, key_itm_nbr, HS_ini, HS_fin, TAX_INI, TAX_FIN,
         DELTA_TAX, ABS_DELTA_TAX, SENS_DELTA_TAX, risk_level)

head(q5_baisse_tax, 30)
## # A tibble: 30 × 10
##    instanceid key_itm_nbr HS_ini  HS_fin TAX_INI TAX_FIN DELTA_TAX ABS_DELTA_TAX
##         <int>       <int> <chr>   <chr>    <dbl>   <dbl>     <dbl>         <dbl>
##  1    2692787           1 871130… 87032…  2.27e6  6.25e5 -1648632.      1648632.
##  2    2692785           1 871130… 87032…  2.01e6  3.59e5 -1648451.      1648451.
##  3    2754985           1 151190… 15119…  2.98e6  1.36e6 -1615691.      1615691.
##  4    2824886           1 730900… 85013…  1.47e6  1.44e5 -1330294.      1330294.
##  5    2793008           1 110100… 11031…  7.38e5  2.46e5  -491919.       491919.
##  6    2893655           1 870323… 87032…  1.80e6  1.37e6  -429770.       429770.
##  7    2820812           1 040291… 34011…  1.30e6  9.24e5  -375743.       375743.
##  8    2732877           1 340119… 11031…  5.60e5  2.92e5  -267927.       267927.
##  9    2696851           1 870323… 87032…  6.49e5  3.90e5  -258366.       258366.
## 10    2741786           1 110100… 19021…  4.85e5  2.59e5  -225982.       225982.
## # ℹ 20 more rows
## # ℹ 2 more variables: SENS_DELTA_TAX <chr>, risk_level <chr>
q5_compare_sens <- item_compare_full %>%
  filter(chg_hs == TRUE) %>%
  count(SENS_DELTA_TAX, risk_level, sort = TRUE, name = "nb_items")

q5_baisse_bureau <- item_compare_full %>%
  filter(chg_hs == TRUE, DELTA_TAX < 0) %>%
  count(ide_cuo_cod, ide_cuo_nam, sort = TRUE, name = "nb_items_baisse_tax")

q5_baisse_owner <- item_compare_full %>%
  filter(chg_hs == TRUE, DELTA_TAX < 0) %>%
  count(owner, sort = TRUE, name = "nb_items_baisse_tax")

q5_compare_sens
## # A tibble: 4 × 3
##   SENS_DELTA_TAX risk_level nb_items
##   <chr>          <chr>         <int>
## 1 STABLE         FAIBLE          182
## 2 HAUSSE_TAX     FORT            130
## 3 STABLE         MOYEN           108
## 4 BAISSE_TAX     FORT             31
head(q5_baisse_bureau, 20)
## # A tibble: 11 × 3
##    ide_cuo_cod  ide_cuo_nam  nb_items_baisse_tax
##    <chr>        <chr>                      <int>
##  1 CUO_FA720F78 CUO_14AB4E59                   9
##  2 CUO_313A6A2E CUO_CA6A5B1B                   3
##  3 CUO_8F4D3372 CUO_BBFD7BA3                   3
##  4 CUO_A330C94C CUO_B9BD7F9F                   3
##  5 CUO_537C11F3 CUO_6656FDFD                   2
##  6 CUO_57FDEDA7 CUO_F674DC97                   2
##  7 CUO_61E48A76 CUO_83E92C90                   2
##  8 CUO_6BC153ED CUO_DFC2161D                   2
##  9 CUO_8D6D6EF8 CUO_014E9359                   2
## 10 CUO_E01087D7 CUO_3355DC7F                   2
## 11 CUO_BA4A1571 CUO_54D892CD                   1
head(q5_baisse_owner, 20)
## # A tibble: 16 × 2
##    owner        nb_items_baisse_tax
##    <chr>                      <int>
##  1 COD_1D44BB6E                   4
##  2 COD_3D42F48C                   4
##  3 COD_91A0D8A8                   4
##  4 COD_6AE100D0                   3
##  5 COD_DC31D3E2                   3
##  6 COD_2A7D61D5                   2
##  7 COD_43C7B650                   2
##  8 COD_1E20F0CA                   1
##  9 COD_5D5A6B83                   1
## 10 COD_6805DBE3                   1
## 11 COD_6BAB4A7E                   1
## 12 COD_6CFDEE48                   1
## 13 COD_9A9822B9                   1
## 14 COD_C2EFE914                   1
## 15 COD_D04DB0EF                   1
## 16 COD_D46603E0                   1

TODO étudiant :

  1. Quelle est la proportion de cas HAUSSE_TAX vs BAISSE_TAX parmi les articles avec HS modifié ? Interpréter cette répartition.
  2. Les cas BAISSE_TAX + FORT méritent une attention particulière. Pourquoi ? Donner 2 interprétations possibles d’un reclassement HS conduisant à une baisse de taxes.
  3. Identifier les bureaux les plus représentés dans les cas de baisse. Un même bureau concentre-t-il les cas de hausse et de baisse ?

12.6 Q6 — Examiner le lien entre exa_exa et owner

Objectif métier : décrire les couples vérificateur / code déclarant les plus fréquents, vérifier si ces couples sont concentrés dans certains bureaux. Rappel : owner est le code déclarant ; end_user (acteur technique réel) n’est pas analysé dans cet atelier.

Sortie attendue : tableau des couples exa_exa / owner les plus fréquents ; synthèse par ide_cuo_cod.

q6_couples <- df_gen %>%
  filter(!is.na(owner)) %>%
  count(ide_cuo_cod, ide_cuo_nam, exa_exa, owner, name = "nb_operations") %>%
  arrange(desc(nb_operations))

head(q6_couples, 20)
##     ide_cuo_cod  ide_cuo_nam     exa_exa        owner nb_operations
## 1  CUO_8062B850 CUO_499564F0 No examiner COD_10189223         13990
## 2  CUO_FA720F78 CUO_14AB4E59 No examiner COD_91A0D8A8         12950
## 3  CUO_61E48A76 CUO_83E92C90 No examiner COD_DC31D3E2         10257
## 4  CUO_E01087D7 CUO_3355DC7F No examiner COD_1D44BB6E          9788
## 5  CUO_E01087D7 CUO_3355DC7F No examiner COD_10189223          9581
## 6  CUO_313A6A2E CUO_CA6A5B1B No examiner COD_6CFDEE48          8391
## 7  CUO_57FDEDA7 CUO_F674DC97 No examiner COD_9CA665A8          7829
## 8  CUO_88C4B39F CUO_7145CEDD No examiner COD_819CF064          7798
## 9  CUO_313A6A2E CUO_CA6A5B1B No examiner COD_1D44BB6E          6869
## 10 CUO_88C4B39F CUO_7145CEDD No examiner COD_2A7D61D5          6607
## 11 CUO_6BC153ED CUO_DFC2161D No examiner COD_819CF064          6229
## 12 CUO_250974A4 CUO_C99A3DF6 No examiner COD_5D5A6B83          6170
## 13 CUO_6BC153ED CUO_DFC2161D No examiner COD_6CFDEE48          5668
## 14 CUO_D08406A2 CUO_3061FD94 No examiner COD_7AB3C08E          5651
## 15 CUO_D08406A2 CUO_3061FD94 No examiner COD_6CFDEE48          5636
## 16 CUO_537C11F3 CUO_6656FDFD No examiner COD_95EBB17A          5631
## 17 CUO_FA720F78 CUO_14AB4E59 No examiner COD_3D42F48C          5566
## 18 CUO_FA720F78 CUO_14AB4E59 No examiner COD_5D5A6B83          5563
## 19 CUO_313A6A2E CUO_CA6A5B1B No examiner COD_7E58B39D          5449
## 20 CUO_08824E78 CUO_B80496FD No examiner COD_3382CCAB          5225
q6_no_examiner_bureau <- df_gen %>%
  filter(!is.na(owner)) %>%
  mutate(no_examiner = exa_exa == "No examiner") %>%
  count(ide_cuo_cod, ide_cuo_nam, no_examiner, name = "nb_operations") %>%
  tidyr::pivot_wider(
    names_from   = no_examiner,
    values_from  = nb_operations,
    values_fill  = 0,
    names_prefix = "no_examiner_"
  ) %>%
  rename(
    nb_operations_exa_renseigne = no_examiner_FALSE,
    nb_operations_no_examiner   = no_examiner_TRUE
  ) %>%
  mutate(
    nb_operations_total = nb_operations_exa_renseigne + nb_operations_no_examiner,
    part_no_examiner    = nb_operations_no_examiner / nb_operations_total
  ) %>%
  arrange(desc(part_no_examiner), desc(nb_operations_no_examiner))

q6_couples_renseignes <- df_gen %>%
  filter(!is.na(owner), exa_exa != "No examiner") %>%
  count(ide_cuo_cod, ide_cuo_nam, exa_exa, owner, name = "nb_operations") %>%
  arrange(desc(nb_operations))

q6_bureau_renseignes <- q6_couples_renseignes %>%
  group_by(ide_cuo_cod, ide_cuo_nam) %>%
  summarise(
    nb_couples_renseignes        = n(),
    nb_operations_renseignees    = sum(nb_operations, na.rm = TRUE),
    max_operations_sur_un_couple = max(nb_operations, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(max_operations_sur_un_couple), desc(nb_operations_renseignees))

head(q6_no_examiner_bureau, 20)
## # A tibble: 20 × 6
##    ide_cuo_cod  ide_cuo_nam  nb_operations_no_examiner nb_operations_exa_rense…¹
##    <chr>        <chr>                            <int>                     <int>
##  1 CUO_88C4B39F CUO_7145CEDD                     48704                         0
##  2 CUO_1B047186 CUO_0A52E7E3                      4348                         0
##  3 CUO_2EF99218 CUO_83A56DC6                      1758                         0
##  4 CUO_26F930EB CUO_C3543505                      1618                         0
##  5 CUO_860710AE CUO_C0695F99                      1420                         0
##  6 CUO_C13F951D CUO_C4FC5C52                      1198                         0
##  7 CUO_A7A73784 CUO_8A211420                      1085                         0
##  8 CUO_4D10587C CUO_71BD6C81                      1084                         0
##  9 CUO_5EA92951 CUO_C7E5CFFF                       599                         0
## 10 CUO_B701682B CUO_63C53650                       493                         0
## 11 CUO_A3F0385F CUO_A1702257                       190                         0
## 12 CUO_0631B89C CUO_A8372852                       174                         0
## 13 CUO_F493F68F CUO_E274C758                       103                         0
## 14 CUO_F0521822 CUO_D4608D63                        49                         0
## 15 CUO_AE023546 CUO_7D2DB62F                        28                         0
## 16 CUO_71E210E2 CUO_5954CF36                        12                         0
## 17 CUO_0A35421D CUO_24BA7FBE                         4                         0
## 18 CUO_08824E78 CUO_B80496FD                      5387                         1
## 19 CUO_BA4A1571 CUO_54D892CD                     13378                      2302
## 20 CUO_6BC153ED CUO_DFC2161D                     47598                      9206
## # ℹ abbreviated name: ¹​nb_operations_exa_renseigne
## # ℹ 2 more variables: nb_operations_total <int>, part_no_examiner <dbl>
head(q6_couples_renseignes, 20)
##     ide_cuo_cod  ide_cuo_nam      exa_exa        owner nb_operations
## 1  CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_29D2B98B          2239
## 2  CUO_250974A4 CUO_C99A3DF6 EXA_0030EA59 COD_5D5A6B83          2033
## 3  CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_1D44BB6E          1665
## 4  CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_E99FB750          1403
## 5  CUO_8062B850 CUO_499564F0 EXA_059C22C2 COD_10189223          1304
## 6  CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_347779D3          1023
## 7  CUO_8062B850 CUO_499564F0 EXA_F58A7E1A COD_10189223          1009
## 8  CUO_313A6A2E CUO_CA6A5B1B EXA_86AA87FC COD_6CFDEE48           973
## 9  CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_7E58B39D           959
## 10 CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_C3D3F58C           882
## 11 CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_91A0D8A8           840
## 12 CUO_250974A4 CUO_C99A3DF6 EXA_0030EA59 COD_37901709           767
## 13 CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_6CFDEE48           697
## 14 CUO_313A6A2E CUO_CA6A5B1B EXA_120AE2A6 COD_6CFDEE48           692
## 15 CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_B51ECEFB           669
## 16 CUO_250974A4 CUO_C99A3DF6 EXA_0030EA59 COD_6CFDEE48           569
## 17 CUO_8062B850 CUO_499564F0 EXA_4F21B130 COD_10189223           521
## 18 CUO_8062B850 CUO_499564F0 EXA_8B810B00 COD_10189223           518
## 19 CUO_313A6A2E CUO_CA6A5B1B EXA_7A3DC350 COD_2376C426           517
## 20 CUO_57FDEDA7 CUO_F674DC97 EXA_296A114A COD_9CA665A8           516
q6_bureau_renseignes
## # A tibble: 20 × 5
##    ide_cuo_cod  ide_cuo_nam  nb_couples_renseignes nb_operations_renseignees
##    <chr>        <chr>                        <int>                     <int>
##  1 CUO_313A6A2E CUO_CA6A5B1B                   141                     22246
##  2 CUO_250974A4 CUO_C99A3DF6                    19                      4498
##  3 CUO_8062B850 CUO_499564F0                   221                     12981
##  4 CUO_57FDEDA7 CUO_F674DC97                   134                      9917
##  5 CUO_FA720F78 CUO_14AB4E59                   599                     13616
##  6 CUO_61E48A76 CUO_83E92C90                   261                      5755
##  7 CUO_8D6D6EF8 CUO_014E9359                    75                      4466
##  8 CUO_E01087D7 CUO_3355DC7F                   242                      6102
##  9 CUO_8F4D3372 CUO_BBFD7BA3                   112                      4218
## 10 CUO_A4FFC1CE CUO_8AC30101                    94                      2376
## 11 CUO_9294067C CUO_5A8393D7                    45                       578
## 12 CUO_A330C94C CUO_B9BD7F9F                    41                       960
## 13 CUO_537C11F3 CUO_6656FDFD                   687                      4872
## 14 CUO_BA4A1571 CUO_54D892CD                   322                      2302
## 15 CUO_6BC153ED CUO_DFC2161D                   998                      9206
## 16 CUO_D08406A2 CUO_3061FD94                  1653                     14315
## 17 CUO_BA4A1571 CUO_E48E7BBF                     9                        25
## 18 CUO_6BC153ED CUO_5EC09149                    11                        18
## 19 CUO_FA720F78 CUO_D41BEFDA                    11                        22
## 20 CUO_08824E78 CUO_B80496FD                     1                         1
## # ℹ 1 more variable: max_operations_sur_un_couple <int>

TODO étudiant :

  1. Quels bureaux affichent un taux No examiner élevé (proche de 100 %) ? Proposer deux interprétations possibles (circuit sans contrôle vs saisie incomplète).
  2. Parmi les couples exa_exa / owner renseignés, identifier les plus fréquents. Que peut signifier une forte récurrence d’un même couple vérificateur / déclarant dans un même bureau ?
  3. Pourquoi est-il important de distinguer owner (code déclarant) de end_user (acteur technique) dans cette analyse ?

13 Bonus — 4 ICP

icp <- decl_workflow %>%
  summarise(
    ICP1_liquidation_jour_meme     = mean(as.Date(date_payment) == as.Date(date_depot), na.rm = TRUE),
    ICP2_mainlevee_le_2j           = mean(delay_pay_bae_days <= 2, na.rm = TRUE),
    ICP3_efficacite_repression     = mean(instanceid %in% (item_compare_full %>%
                                            filter(DELTA_TAX > 0) %>% pull(instanceid)), na.rm = TRUE),
    ICP4_contribution_recouvrement = mean(item_compare_full$DELTA_TAX[item_compare_full$DELTA_TAX > 0],
                                          na.rm = TRUE)
  )

icp
## # A tibble: 1 × 4
##   ICP1_liquidation_jour_meme ICP2_mainlevee_le_2j ICP3_efficacite_repression
##                        <dbl>                <dbl>                      <dbl>
## 1                      0.485                0.941                    0.00623
## # ℹ 1 more variable: ICP4_contribution_recouvrement <dbl>

TODO étudiant :

  1. Interpréter chacun des 4 ICP en une phrase : que mesure-t-il, quelle est la valeur obtenue, est-elle favorable ou défavorable ?
  2. ICP3 mesure le taux de déclarations avec au moins un redressement positif. Quelle nuance faut-il apporter à l’interprétation de ce taux (un taux faible signifie-t-il forcément un système très conforme) ?
  3. ICP4 est une moyenne. Pourquoi la médiane serait-elle un indicateur complémentaire utile ici ?

14 Nettoyage mémoire

rm(item_fl, tax_fl, tax_item_ver, tax_item_ver_code)
gc()
##            used  (Mb) gc trigger   (Mb)  max used   (Mb)
## Ncells  1823550  97.4   14426543  770.5  35221048 1881.1
## Vcells 45841239 349.8  158741114 1211.1 198426392 1513.9