精度を結果指標として使用する場合の例は誤った結論につながる


8

予測モデルのさまざまなパフォーマンス指標を調べています。モデルのパフォーマンスを評価するためのより継続的なものではなく、精度の使用に関する問題について多くが書かれました。Frank Harrell http://www.fharrell.com/post/class-damage/は、モデルに有益な変数を追加すると、精度が低下し、明らかに直観に反し、誤った結論に至る場合の例を示しています。

ここに画像の説明を入力してください

ただし、この場合は、クラスの不均衡が原因であると考えられるため、代わりに((sens + spec)/ 2)のバランスの取れた精度を使用するだけで解決できます。バランスのとれたデータセットで精度を使用すると、明らかに間違った、または直観に反する結論につながる例はありますか?

編集する

モデルが明らかに優れている場合でも精度が低下するもの、または精度を使用すると一部の機能が誤って選択される可能性があるものを探しています。2つのモデルの精度が同じで、他の基準を使用した方が明らかに優れている偽陰性の例を作成するのは簡単です。


4
関連するが重複ではない
ステファンコラサ

2
上記にリンクされているステファンの投稿は素晴らしいリソースであり、必要なすべてを備えています。強制分類(時期尚早の決定)が必要であると想定する最初のステップで問題が発生しました。また、(sens + spec)/ 2は適切な精度スコアではありません。それを最適化すると、間違った機能を選択し、それらに間違った重みを与えることにつながります。「決定なし」ゾーンなどの確率から得られる有用な情報を無視することは言うまでもありません。
フランクハレル2018

確率的予測の価値は理解していますが、バランスの取れたデータまたは正確な精度で実際に発生したこれらの悪いことが発生する例を探しています。
rep_ho 2018

ところで:Gneiting&Raftery 2007によると、精度は適切です(厳密には適切ではありません)。それについてのコメントは?amstat.tandfonline.com/doi/abs/10.1198/…– rep_ho 2018
11:47

私の回答がお役に立てば幸いです。(@FrankHarrell:コメントは歓迎します。)バランスの取れた正確さもここでは役に立ちません。正確さを適切ではあるが厳密には適切ではないスコアリングルールについて:バイナリ分類の設定は、精度は不適切なスコアリングルールですか?そのスレッドがあなたの質問に答えない場合は、新しい質問をすることを検討してください(そして、既存のスレッドが答えではないのでそれがだまされてクローズされない理由を指摘します)。
ステファンコラサ

回答:


13

私はごまかします。

具体的には、私が(例えば、しばしば主張してきたここにいること)、統計的モデリングと予測の一部のみが行うことにまで及ぶ確率論を、クラスメンバーシップの予測(または数値予測の場合は予測密度の提供)のました。特定のインスタンスを特定のクラス(または数値の場合はポイント予測)に属しているかのように扱うと、統計が適切に機能しなくなります。それは決定理論の側面の一部です。

また、決定は、確率論的予測に基づくだけでなく、誤分類のコストや、その他の多くの可能なアクションにも基づくべきです。たとえば、「病気」と「健康」の2つのクラスしかない場合でも、患者が病気にかかっている可能性に応じて、彼が家に帰るという理由で、さまざまなアクションを実行できます。彼に2つのアスピリンを与えること、追加のテストを実行すること、救急車をすぐに呼び出して彼を生命維持に置くことはほぼ間違いなく健康です。

精度の評価は、このような決定を前提としています。分類の評価指標としての精度は、カテゴリエラーです。

だから、あなたの質問に答えるために、私はそのようなカテゴリーエラーのパスを歩きます。誤った分類のコストを考慮せずに分類すると、実際に私たちをひどく誤解させる、バランスのとれたクラスの単純なシナリオを検討します。


悪性グトロットの流行が人口で蔓延していると仮定します。幸いなことに、我々はいくつかの形質のために簡単に皆をスクリーニングすることができるt0t1)、そして我々はMGを開発する確率はに直線的に依存していることを知っているtp=γt、いくつかのパラメータのためのγ0γ1)。特性tは母集団に均一に分布しています。

幸いなことに、ワクチンがあります。残念ながら、それは高価であり、副作用は非常に不快です。(私はあなたの想像力が詳細を提供するようにします。)しかし、それらはMGに苦しむよりはましです。

抽象化のために、特性値tが与えられた場合、特定の患者には実際には2つの可能なコースしかないと考えます。tます。

したがって、問題は次のとおりですt与えられたら、誰に予防接種をし、誰に予防接種を行わないかをどのように決定すべきですか?私たちは次のようになります功利この程度と最低の総期待コストを持つことを目的としています。しきい値の選択に降りてくることは明らかであるθとしてワクチン接種全員にtθ


Model-and-Decision 1は、精度に基づいています。モデルをフィットさせます。幸い、私たちはすでにモデルを知っています。しきい値選びθ精度を最大限に患者を分類するときと、およびワクチン接種みんなtθ。私たちは、簡単に見ることθ=12γマジックナンバーである-と誰もtθないよりMGを収縮の高いチャンスがあるし、その逆もまた同様で、ので、この分類確率閾値は、精度を最大化します。バランスのとれたクラス、γ=1と仮定すると、人口の半分にワクチン接種します。おかしなことに、γ<1の場合γ<12、私たちは誰にも予防接種をしません。(私たちは主にバランスの取れたクラスに興味を持っているので、人口の一部だけが恐ろしい痛みを伴う死に瀕させていることを無視しましょう。)

言うまでもなく、これは誤分類の差異コストを考慮に入れていません。


モデル・アンド・ディシジョン2は、確率的予測(「特性t与えられ、MGが収縮する確率はγt」)コスト構造の両方を活用します。

まず、ここに小さなグラフがあります。横軸は特性を示し、縦軸はMG確率を示します。影付きの三角形は、MGを契約する人口の割合を示します。垂直線は特定のθます。で、水平の破線γθ少し単純下記の計算が従うことになります。γ > 1と仮定しますγ>12、人生を楽にするためだけに。

分類

θγ(および特性が母集団内で均一に分布しているという事実)を前提として、コストの名前を与え、それらの合計予想コストへの寄与を計算してみましょう。

  • してみましょうc++ワクチン接種され、MGを契約していた患者のためのコストを示します。与えられたθ、この費用を負担人口の割合は、面積と右下の日陰台形である
    1θγθ+121θγγθ
  • してみましょうc+ワクチン接種されてしまう患者のためのコストを表していない MGを契約しています。与えられたθ、この費用を負担人口の割合は、地域との右上に影のない台形である
    1θ1γ+121θγγθ
  • してみましょうcされる患者のためのコストを表していないワクチン接種となりません MGを契約しています。与えられたθ、この費用を負担人口の割合は、上部の影のない台形の面積を残している
    θ1γθ+12θγθ
  • してみましょうc+された患者のための意味のコストではないワクチン接種とMGを契約しているだろう。与えられたθ、この費用を負担人口の割合が面積で左下に影付き三角形である
    12θγθ

(各台形では、まず長方形の面積を計算してから、三角形の面積を追加します。)

合計コストがあると予想

c++1θγθ+121θγγθ+c+1θ1γ+121θγγθ+cθ1γθ+12θγθ+c+12θγθ

微分を微分してゼロに設定すると、予想されるコストがθ = c +c によって最小化されることがわかります。

θ=c+cγc++c+c++c

これは、非常に具体的なコスト構造、つまり1の場合に限り、 θ精度最大化値にのみ等しくなります。

12γ=c+cγc++c+c++c
または
12=c+cc++c+c++c

例として、であると仮定します。γ=1

c++=1c+=2c+=10c=0。
θ=121.875θ=2111.318

この例では、精度を最大化する非確率的分類に基づいて決定を行うと、確率的予測のコンテキストで微分コスト構造を明示的に使用する決定ルールを使用するよりも、ワクチン接種とコストが高くなります。


結論:精度は、以下の場合にのみ有効な決定基準です。

  • クラスと可能なアクションの間には1対1の関係があります
  • そしてクラスに適用されるアクションのコストは非常に特定の構造に従ってください。

一般的なケースでは、正確さを評価することは間違った質問をします、そして正確さを最大にすることはいわゆるタイプIIIエラーです:間違った質問に正しい答えを提供します。


Rコード:

rm(list=ls())
gamma <- 0.7

cost_treated_positive <- 1          # cost of treatment, side effects unimportant
cost_treated_negative <- 2          # cost of treatment, side effects unnecessary
cost_untreated_positive <- 10       # horrible, painful death
cost_untreated_negative <- 0        # nothing

expected_cost <- function ( theta ) {
    cost_treated_positive * ( (1-theta)*theta*gamma + (1-theta)*(gamma-gamma*theta)/2 ) +
    cost_treated_negative * ( (1-theta)*(1-gamma) + (1-theta)*(gamma-gamma*theta)/2 ) +
    cost_untreated_negative *( theta*(1-gamma*theta) + theta*gamma*theta/2 ) +
    cost_untreated_positive * theta*gamma*theta/2
}

(theta <- optim(par=0.5,fn=expected_cost,lower=0,upper=1,method="L-BFGS-B")$par)
(cost_treated_negative-cost_untreated_negative)/
    (gamma*(cost_treated_negative+cost_untreated_positive-cost_treated_positive-cost_untreated_negative))

plot(c(0,1),c(0,1),type="n",bty="n",xaxt="n",xlab="Trait t",yaxt="n",ylab="MG probability")
rect(0,0,1,1)
axis(1,c(0,theta,1),c(0,"theta",1),lty=0,line=-1)
axis(2,c(0,1),lty=0,line=-1,las=1)
axis(4,c(0,gamma,1),c(0,"gamma",1),lty=0,line=-1.8,las=1)
polygon(c(0,1,1),c(0,0,gamma),col="lightgray")
abline(v=theta,col="red",lwd=2)
abline(h=gamma*theta,lty=2,col="red",lwd=2)

expected_cost(1/(2*gamma))
expected_cost(theta)

1
素晴らしいポスト!ログインして感謝の意を表します!
Wolfone

「しかし、誤分類のコストにも」私はそれを本当だとは思いません:あなたの計算自体が示すように、それはまた(驚くことに!)正しい分類のコストにも依存します!
Tamas Ferenci

cd+=c+c++cd=c+cθ=cdγcd+cd+

また、このプロットを作ることができる:levelplot( thetastar ~ cdminus + cdplus, data = data.table( expand.grid( cdminus = seq( 0, 10, 0.01 ), cdplus = seq( 0, 10, 0.01 ) ) )[ , .( cdminus, cdplus, thetastar = cdminus/(cdminus + cdplus) ) ] )
タマスFerenci

θ=1/γ1+cd+cd

4

スティーブンの優れた答えに別の、おそらくもっと簡単な例を追加する価値があるかもしれません。

T|DNμσ2T|DNμ+σ2
pDBerp

bb

DDTp1Φ+b1p1ΦbTpΦ+b1pΦb


精度ベースのアプローチ

p1Φ+b+1pΦb

b1πσ2

pφ+b+φbpφb=0ebμ22σ2[1ppe2bμμ++μ+2μ22σ2]=0
1ppe2bμμ++μ+2μ22σ2=02bμμ++μ+2μ22σ2=ログ1pp2bμ+μ+μ2μ+2=2σ2ログ1pp
b=μ+2μ2+2σ2ログ1pp2μ+μ=μ++μ2+σ2μ+μログ1pp

これは-もちろん-費用に依存しないことに注意してください。

クラスがバランスしている場合、最適は、病気の人と健康な人の平均テスト値の平均です。それ以外の場合は、不均衡に基づいて置き換えられます。


コストベースのアプローチ

c++p1Φ+b+c+1p1Φb+c+pΦ+b+c1pΦb
b
c++pφ+(b)c+(1p)φ(b)+c+pφ+(b)+c(1p)φ(b)==φ+(b)p(c+c++)+φ(b)(1p)(cc+)==φ+(b)pcd+φ(b)(1p)cd=0,
using the notation I introduced in my comments below Stephen's answer, i.e., cd+=c+c++ and cd=c+c.

The optimal threshold is therefore given by the solution of the equation

φ+(b)φ(b)=(1p)cdpcd+.
Two things should be noted here:

  1. This results is totally generic and works for any distribution of the test results, not only normal. (φ in that case of course means the probability density function of the distribution, not the normal density.)
  2. Whatever the solution for b is, it is surely a function of (1p)cdpcd+. (I.e., we immediately see how costs matter - in addition to class imbalance!)

I'd be really interested to see if this equation has a generic solution for b (parametrized by the φs), but I would be surprised.

Nevertheless, we can work it out for normal! 2πσ2s cancel on the left hand side, so we have

e12((bμ+)2σ2(bμ)2σ2)=(1p)cdpcd+(bμ)2(bμ+)2=2σ2log(1p)cdpcd+2b(μ+μ)+(μ2μ+2)=2σ2log(1p)cdpcd+
therefore the solution is
b=(μ+2μ2)+2σ2log(1p)cdpcd+2(μ+μ)=μ++μ2+σ2μ+μlog(1p)cdpcd+.

(Compare it the the previous result! We see that they are equal if and only if cd=cd+, i.e. the differences in misclassification cost compared to the cost of correct classification is the same in sick and healthy people.)


A short demonstration

Let's say c=0 (it is quite natural medically), and that c++=1 (we can always obtain it by dividing the costs with c++, i.e., by measuring every cost in c++ units). Let's say that the prevalence is p=0.2. Also, let's say that μ=9.5, μ+=10.5 and σ=1.

In this case:

library( data.table )
library( lattice )

cminusminus <- 0
cplusplus <- 1
p <- 0.2
muminus <- 9.5
muplus <- 10.5
sigma <- 1

res <- data.table( expand.grid( b = seq( 6, 17, 0.1 ),
                                cplusminus = c( 1, 5, 10, 50, 100 ),
                                cminusplus = c( 2, 5, 10, 50, 100 ) ) )
res$cost <- cplusplus*p*( 1-pnorm( res$b, muplus, sigma ) ) +
  res$cplusminus*(1-p)*(1-pnorm( res$b, muminus, sigma ) ) +
  res$cminusplus*p*pnorm( res$b, muplus, sigma ) +
  cminusminus*(1-p)*pnorm( res$b, muminus, sigma )

xyplot( cost ~ b | factor( cminusplus ), groups = cplusminus, ylim = c( -1, 22 ),
        data = res, type = "l", xlab = "Threshold",
        ylab = "Expected overall cost", as.table = TRUE,
        abline = list( v = (muplus+muminus)/2+
                         sigma^2/(muplus-muminus)*log((1-p)/p) ),
        strip = strip.custom( var.name = expression( {"c"^{"+"}}["-"] ),
                              strip.names = c( TRUE, TRUE ) ),
        auto.key = list( space = "right", points = FALSE, lines = TRUE,
                         title = expression( {"c"^{"-"}}["+"] ) ),
        panel = panel.superpose, panel.groups = function( x, y, col.line, ... ) {
          panel.xyplot( x, y, col.line = col.line, ... )
          panel.points( x[ which.min( y ) ], min( y ), pch = 19, col = col.line )
        } )

The result is (points depict the minimum cost, and the vertical line shows the optimal threshold with the accuracy-based approach):

Expected overall cost

We can very nicely see how cost-based optimum can be different than the accuracy-based optimum. It is instructive to think over why: if it is more costly to classify a sick people erroneously healthy than the other way around (c+ is high, c+ is low) than the threshold goes down, as we prefer to classify more easily into the category sick, on the other hand, if it is more costly to classify a healthy people erroneously sick than the other way around (c+ is low, c+ is high) than the threshold goes up, as we prefer to classify more easily into the category healthy. (Check these on the figure!)


A real-life example

Let's have a look at an empirical example, instead of a theoretical derivation. This example will be different basically from two aspects:

  • Instead of assuming normality, we will simply use the empirical data without any such assumption.
  • Instead of using one single test, and its results in its own units, we will use several tests (and combine them with a logistic regression). Threshold will be given to the final predicted probability. This is actually the preferred approach, see Chapter 19 - Diagnosis - in Frank Harrell's BBR.

The dataset (acath from the package Hmisc) is from the Duke University Cardiovascular Disease Databank, and contains whether the patient had significant coronary disease, as assessed by cardiac catheterization, this will be our gold standard, i.e., the true disease status, and the "test" will be the combination of the subject's age, sex, cholesterol level and duration of symptoms:

library( rms )
library( lattice )
library( latticeExtra )
library( data.table )

getHdata( "acath" )
acath <- acath[ !is.na( acath$choleste ), ]
dd <- datadist( acath )
options( datadist = "dd" )

fit <- lrm( sigdz ~ rcs( age )*sex + rcs( choleste ) + cad.dur, data = acath )

It worth plotting the predicted risks on logit-scale, to see how normal they are (essentially, that was what we assumed previously, with one single test!):

densityplot( ~predict( fit ), groups = acath$sigdz, plot.points = FALSE, ref = TRUE,
             auto.key = list( columns = 2 ) )

Distribution of predicted risks

Well, they're hardly normal...

Let's go on and calculate the expected overall cost:

ExpectedOverallCost <- function( b, p, y, cplusminus, cminusplus,
                                 cplusplus = 1, cminusminus = 0 ) {
  sum( table( factor( p>b, levels = c( FALSE, TRUE ) ), y )*matrix(
    c( cminusminus, cplusminus, cminusplus, cplusplus ), nc = 2 ) )
}

table( predict( fit, type = "fitted" )>0.5, acath$sigdz )

ExpectedOverallCost( 0.5, predict( fit, type = "fitted" ), acath$sigdz, 2, 4 )

And let's plot it for all possible costs (a computational note: we don't need to mindlessly iterate through numbers from 0 to 1, we can perfectly reconstruct the curve by calculating it for all unique values of predicted probabilities):

ps <- sort( unique( c( 0, 1, predict( fit, type = "fitted" ) ) ) )

xyplot( sapply( ps, ExpectedOverallCost,
                p = predict( fit, type = "fitted" ), y = acath$sigdz,
                cplusminus = 2, cminusplus = 4 ) ~ ps, type = "l", xlab = "Threshold",
        ylab = "Expected overall cost", panel = function( x, y, ... ) {
          panel.xyplot( x, y, ... )
          panel.points( x[ which.min( y ) ], min( y ), pch = 19, cex = 1.1 )
          panel.text( x[ which.min( y ) ], min( y ), round( x[ which.min( y ) ], 3 ),
                      pos = 3 )
        } )

Expected overall cost as a function of threshold

We can very well see where we should put the threshold to optimize the expected overall cost (without using sensitivity, specificity or predictive values anywhere!). This is the correct approach.

It is especially instructive to contrast these metrics:

ExpectedOverallCost2 <- function( b, p, y, cplusminus, cminusplus,
                                  cplusplus = 1, cminusminus = 0 ) {
  tab <- table( factor( p>b, levels = c( FALSE, TRUE ) ), y )
  sens <- tab[ 2, 2 ] / sum( tab[ , 2 ] )
  spec <- tab[ 1, 1 ] / sum( tab[ , 1 ] )
  c( `Expected overall cost` = sum( tab*matrix( c( cminusminus, cplusminus, cminusplus,
                                                   cplusplus ), nc = 2 ) ),
     Sensitivity = sens,
     Specificity = spec,
     PPV = tab[ 2, 2 ] / sum( tab[ 2, ] ),
     NPV = tab[ 1, 1 ] / sum( tab[ 1, ] ),
     Accuracy = 1 - ( tab[ 1, 1 ] + tab[ 2, 2 ] )/sum( tab ),
     Youden = 1 - ( sens + spec - 1 ),
     Topleft = ( 1-sens )^2 + ( 1-spec )^2
  )
}

ExpectedOverallCost2( 0.5, predict( fit, type = "fitted" ), acath$sigdz, 2, 4 )

res <- melt( data.table( ps, t( sapply( ps, ExpectedOverallCost2,
                                        p = predict( fit, type = "fitted" ),
                                        y = acath$sigdz,
                                        cplusminus = 2, cminusplus = 4 ) ) ),
             id.vars = "ps" )

p1 <- xyplot( value ~ ps, data = res, subset = variable=="Expected overall cost",
              type = "l", xlab = "Threshold", ylab = "Expected overall cost",
              panel=function( x, y, ... ) {
                panel.xyplot( x, y,  ... )
                panel.abline( v = x[ which.min( y ) ],
                              col = trellis.par.get()$plot.line$col )
                panel.points( x[ which.min( y ) ], min( y ), pch = 19 )
              }  )
p2 <- xyplot( value ~ ps, groups = variable,
              data = droplevels( res[ variable%in%c( "Expected overall cost",
                                                     "Sensitivity",
                                                     "Specificity", "PPV", "NPV" ) ] ),
              subset = variable%in%c( "Sensitivity", "Specificity", "PPV", "NPV" ),
              type = "l", xlab = "Threshold", ylab = "Sensitivity/Specificity/PPV/NPV",
              auto.key = list( columns = 3, points = FALSE, lines = TRUE ) )
doubleYScale( p1, p2, use.style = FALSE, add.ylab2 = TRUE )

Expected overall cost and traditional metrics as a function of threshold

We can now analyze those metrics that are sometimes specifically advertised as being able to come up with an optimal cutoff without costs, and contrast it with our cost-based approach! Let's use the three most often used metrics:

  • Accuracy (maximize accuracy)
  • Youden rule (maximize Sens+Spec1)
  • Topleft rule (minimize (1Sens)2+(1Spec)2)

(For simplicity, we will subtract the above values from 1 for the Youden and the Accuracy rule so that we have a minimization problem everywhere.)

Let's see the results:

p3 <- xyplot( value ~ ps, groups = variable,
              data = droplevels( res[ variable%in%c( "Expected overall cost", "Accuracy",
                                                     "Youden", "Topleft"  ) ] ),
              subset = variable%in%c( "Accuracy", "Youden", "Topleft"  ),
              type = "l", xlab = "Threshold", ylab = "Accuracy/Youden/Topleft",
              auto.key = list( columns = 3, points = FALSE, lines = TRUE ),
              panel = panel.superpose, panel.groups = function( x, y, col.line, ... ) {
                panel.xyplot( x, y, col.line = col.line, ... )
                panel.abline( v = x[ which.min( y ) ], col = col.line )
                panel.points( x[ which.min( y ) ], min( y ), pch = 19, col = col.line )
              } )
doubleYScale( p1, p3, use.style = FALSE, add.ylab2 = TRUE )

Choices to select the optimal cutoff

This of course pertains to one specific cost structure, c=0, c++=1, c+=2, c+=4 (this obviously matters only for the optimal cost decision). To investigate the effect of cost structure, let's pick just the optimal threshold (instead of tracing the whole curve), but plot it as a function of costs. More specifically, as we have already seen, the optimal threshold depends on the four costs only through the cd/cd+ ratio, so let's plot the optimal cutoff as a function of this, along with the typically used metrics that don't use costs:

res2 <- data.frame( rat = 10^( seq( log10( 0.02 ), log10( 50 ), length.out = 500 ) ) )
res2$OptThreshold <- sapply( res2$rat,
                             function( rat ) ps[ which.min(
                               sapply( ps, Vectorize( ExpectedOverallCost, "b" ),
                                       p = predict( fit, type = "fitted" ),
                                       y = acath$sigdz,
                                       cplusminus = rat,
                                       cminusplus = 1,
                                       cplusplus = 0 ) ) ] )

xyplot( OptThreshold ~ rat, data = res2, type = "l", ylim = c( -0.1, 1.1 ),
        xlab = expression( {"c"^{"-"}}["d"]/{"c"^{"+"}}["d"] ), ylab = "Optimal threshold",
        scales = list( x = list( log = 10, at = c( 0.02, 0.05, 0.1, 0.2, 0.5, 1,
                                                   2, 5, 10, 20, 50 ) ) ),
        panel = function( x, y, resin = res[ ,.( ps[ which.min( value ) ] ),
                                             .( variable ) ], ... ) {
          panel.xyplot( x, y, ... )
          panel.abline( h = resin[variable=="Youden"] )
          panel.text( log10( 0.02 ), resin[variable=="Youden"], "Y", pos = 3 )
          panel.abline( h = resin[variable=="Accuracy"] )
          panel.text( log10( 0.02 ), resin[variable=="Accuracy"], "A", pos = 3 )
          panel.abline( h = resin[variable=="Topleft"] )
          panel.text( log10( 0.02 ), resin[variable=="Topleft"], "TL", pos = 1 )
        } )

Optimal thresholds for different costs

Horizontal lines indicate the approaches that don't use costs (and are therefore constant).

Again, we nicely see that as the additional cost of misclassification in the healthy group rises compared to that of the diseased group, the optimal threshold increases: if we really don't want healthy people to be classified as sick, we will use higher cutoff (and the other way around, of course!).

And, finally, we yet again see why those methods that don't use costs are not (and can't!) be always optimal.

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.