2020年12月26日土曜日

Dispatchers.Main.immediate ってなに?

Dispatchers.Main は MainCoroutineDispatcher です。 package kotlinx.coroutines ... public actual object Dispatchers { ... @JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher ... } Dispatchers.Main.immediate は MainCoroutineDispatcher に定義されており、Dispatchers.Main.immediate 自体も MainCoroutineDispatcher です。 package kotlinx.coroutines ... public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { ... public abstract val immediate: MainCoroutineDispatcher ... } kotlinx-coroutines-android では HandlerDispatcher が MainCoroutineDispatcher を継承し、 package kotlinx.coroutines.android ... public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay { ... public abstract override val immediate: HandlerDispatcher } HandlerContext が HandlerDispatcher を継承しています。 package kotlinx.coroutines.android ... internal class HandlerContext private constructor( private val handler: Handler, private val name: String?, private val invokeImmediately: Boolean ) : HandlerDispatcher(), Delay { ... @Volatile private var _immediate: HandlerContext? = if (invokeImmediately) this else null override val immediate: HandlerContext = _immediate ?: HandlerContext(handler, name, true).also { _immediate = it } override fun isDispatchNeeded(context: CoroutineContext): Boolean { return !invokeImmediately || Looper.myLooper() != handler.looper } ... } HandlerContext では immediate にセットされる HandlerContext は invokeImmediately プロパティが true になる、ということがわかります。

invokeImmediately は isDispatchNeeded() で使われます。isDispatchNeeded() の実装をみると、invokeImmediately が false のときは常に isDispatchNeeded() が true を返すことがわかります。また Looper.myLooper() != handler.looper のときも isDispatchNeeded() が true を返すことがわかります。つまり、invokeImmediately が true かつ Looper.myLooper() == handler.looper のときだけ isDispatchNeeded() は false を返します。

このことから、immediate にセットされる HandlerContext では、Looper.myLooper() が handler.looper と同じだと isDispatchNeeded() が false を返すということがわかります。

isDispatchNeeded() は coroutine を dispatch メソッドで実行するべきかどうか判定するときに呼ばれます。デフォルトは true を返すようになっています。 package kotlinx.coroutines ... public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { ... public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true ... }

つまり、(kotlinx-coroutines-android の) Dispatchers.Main.immediate はすでに UI スレッドにいる場合(現在の Looper.myLooper() が handler.looper と同じ場合)そのまますぐに実行される Dispatcher ということです。

例えば Dispatchers.Main を使った以下のコードだと class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Main).launch { println("1 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } 出力は 2 が 1 より先になります。 I/System.out: 2 : main I/System.out: 1 : main CoroutineScope の dispatcher を Dispatchers.Main.immediate に変えると class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Main.immediate).launch { println("1 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } 1 が先に出力されるようになります。 I/System.out: 1 : main I/System.out: 2 : main UI スレッドにいない場合はすぐには実行されず dispatch されます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Default).launch { println("3 : ${Thread.currentThread().name}") CoroutineScope(Dispatchers.Main.immediate).launch { println("1 : ${Thread.currentThread().name}") } println("4 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } I/System.out: 2 : main I/System.out: 3 : DefaultDispatcher-worker-2 I/System.out: 4 : DefaultDispatcher-worker-2 I/System.out: 1 : main

viewModelScope, lifecycleScope は dispatcher として Dispatchers.Main.immediate が指定されています。 val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { ... val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) ... } } val ViewModel.viewModelScope: CoroutineScope get() { ... return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) }



2020年12月24日木曜日

android:fontFamily="sans-serif-medium" の文字の太さを動的に変更する

android:fontFamily に "sans-serif" または "sans-serif-medium"、android:textStyle に "normal" または "bold" を指定したときに生成される Typeface の weight, style, isBold は次のようになります。 <TextView ... android:fontFamily="sans-serif" or "sans-serif-medium" android:textStyle="normal" or "bold" />
fontFamilytextStyleweightstyleisBold
sans-serifnormal4000false
sans-serifbold7001true
sans-serif-mediumnormal5000false
sans-serif-mediumbold8001true

Typeface の style は textStyle の設定が反映されます。style が 1 だと Typeface.BOLD, 2 だと Typeface.ITALIC, 3 だと Typeface.BOLD_ITALIC です。なので style が 1 のとき isBold が true になっています。

"sans-serif-medium" + normal はちょっと太いですが isBold は false です。


android:textStyle の値をプログラムから変更するには TextView.setTypeface() メソッドを使います。このメソッドには引数の異なる2種類があります。 public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); } else { tf = Typeface.create(tf, style); } setTypeface(tf); // now compute what (if any) algorithmic styling is needed int typefaceStyle = tf != null ? tf.getStyle() : 0; int need = style & ~typefaceStyle; mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); } else { mTextPaint.setFakeBoldText(false); mTextPaint.setTextSkewX(0); setTypeface(tf); } } public void setTypeface(@Nullable Typeface tf) { if (mTextPaint.getTypeface() != tf) { mTextPaint.setTypeface(tf); if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } Typeface.Style を渡さない方では mTextPaint に Typeface をセットして invalidate しています。

Typeface.Style を渡す方では、最終的に setTypeface(@Nullable Typeface tf) を呼び出しています。

style が 0 より大きい場合、つまり bold, italic, bold_italic のいずれかのときは Typeface.defaultFromStyle() または Typeface.create() を使って style に対応する Typeface を生成します。 生成した Typeface が style に対応していない場合だけ FakeBold や TextSkewX がセットされます。

style が 0 (= Typeface.NORMAL)の場合、style に対応する Typeface を生成せず、渡された Typeface をそのままセットします。

これによりどういうことが起こるかと言うと、
"sans-serif-medium" + normal な TextView に setTypeface(tv.typeface, Typeface.BOLD) すると bold になるのに、
"sans-serif-medium" + bold な TextView に setTypeface(tv.typeface, Typeface.NORMAL) すると normal にならない!
のです。 mediumNormalTextView.setTypeface(mediumNormalTextView.typeface, Typeface.BOLD) mediumBoldTextView.setTypeface(mediumBoldTextView.typeface, Typeface.NORMAL)

これは "sans-serif-medium" + bold のときに生成される Typeface 自体が bold になっていて(FakeBold を使わない)、style が 0 (= Typeface.NORMAL)の場合は渡した bold な Typeface をそのままセットされてしまうからです。

これを防ぐには、Typeface.create() で Typeface.NORMAL を適用した Typeface を生成して setTypeface() に渡します。 val tf = Typeface.create(mediumBoldTextView.typeface, Typeface.NORMAL) mediumBoldTextView.setTypeface(tf, Typeface.NORMAL)



2020年12月21日月曜日

Jupyter notebooks + Kotlin で移動平均を描画する(その3): 移動平均を計算する

このエントリは Fintalk Advent Calendar 2020 の21日目です。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。


移動平均(Moving Average)は簡単に言うと、時系列データで平均をとって滑らかにする方法です。

Wikipedia にはこう書いてあります。

移動平均()は、時系列データ(より一般的には時系列に限らず系列データ)を平滑化する手法である。音声や画像等のデジタル信号処理に留まらず、金融(特にテクニカル分析)分野、気象、水象を含む計測分野等、広い技術分野で使われる。有限インパルス応答に対するローパスフィルタ(デジタルフィルタ)の一種であり、分野によっては移動積分とも呼ばれる。

主要なものは、単純移動平均と加重移動平均と指数移動平均の3種類である。普通、移動平均といえば、単純移動平均のことをいう。

by https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87


ここに書いてあるとおり、主なものとして
  • 単純移動平均
  • 加重移動平均
  • 指数移動平均
があります。

それぞれ計算してみましょう。
前回CSVデータを操作して得られた日本の新規感染者の最初の20日間のデータを使ってみます。 A DataFrame: 318 x 2 date new_cases_double 1 2020-01-23 0 2 2020-01-24 0 3 2020-01-25 0 4 2020-01-26 2 5 2020-01-27 0 6 2020-01-28 3 7 2020-01-29 0 8 2020-01-30 4 9 2020-01-31 4 10 2020-02-01 5 11 2020-02-02 0 12 2020-02-03 0 13 2020-02-04 2 14 2020-02-05 1 15 2020-02-06 0 16 2020-02-07 0 17 2020-02-08 1 18 2020-02-09 0 19 2020-02-10 2 20 2020-02-11 1 わかりやすいように4日間の移動平均(4日移動平均)を計算してみます。

単純移動平均(Simple Moving Average)

単純移動平均は値をそのまま足して平均を計算する方法です。
例えば 01-26 の単純移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)を足して4で割るので 0.5 です。

01-26 の単純移動平均 = (01-23 + 01-24 + 01-25 + 01-26) / 4 = (0 + 0 + 0 + 2) / 4 = 0.5

01-27 の単純移動平均を計算するとき、01-24 〜 01-27 までのデータ(0,0,2,0)を足して4で割ってもいいのですが、すでに01-26 の単純移動平均が計算してあるなら

01-27 の単純移動平均
= (01-24 + 01-25 + 01-26 + 01-27) / 4
= (- 01-23 + (01-23 + 01-24 + 01-25 + 01-26) + 01-27) / 4
= - 01-23 / 4 + (01-23 + 01-24 + 01-25 + 01-26) / 4 + 01-27 / 4
= - 01-23 / 4 + 01-26 の単純移動平均 + 01-27 / 4
= 01-26 の単純移動平均 - 01-23 / 4 + 01-27 / 4

このように、01-26 の単純移動平均から古い値(01-23)を4で割った数を引いて、新しい値(01-27)を4で割った値を足せば求めることができます。

01-23 〜 01-25 の単純移動平均は4日間分のデータがないので計算できません。

A DataFrame: 318 x 2 date new_cases_double simple_moving_average 1 2020-01-23 0 2 2020-01-24 0 3 2020-01-25 0 4 2020-01-26 2 0.50 5 2020-01-27 0 0.50 6 2020-01-28 3 1.25 7 2020-01-29 0 1.25 8 2020-01-30 4 1.75 9 2020-01-31 4 2.75 10 2020-02-01 5 3.25 11 2020-02-02 0 3.25 12 2020-02-03 0 2.25 13 2020-02-04 2 1.75 14 2020-02-05 1 0.75 15 2020-02-06 0 0.75 16 2020-02-07 0 0.75 17 2020-02-08 1 0.50 18 2020-02-09 0 0.25 19 2020-02-10 2 0.75 20 2020-02-11 1 1.00 val newCases3 = newCases2.head(20) val values = newCases3["new_cases_double"].asDoubles() val size = values.size var sma = arrayOfNulls<Double?>(size) val n = 4 // calculate 01-26 var sum = 0.0 for(i in 0 until n) { sum += values[i]!! } sma[n-1] = sum/n // calculate 01-27 ~ for(i in n until values.size) { sma[i] = sma[i-1]!! - values[i-n]!!/n + values[i]!!/n } val newCases4 = newCases3.addColumn("simple_moving_average") { sma }
赤の単純移動平均の線が滑らかになっているのがわかります。


加重移動平均(Weighted Moving Average)

加重移動平均は各値に重みをつけたものを足して平均を計算する方法です。例えば線形加重移動平均(Linear Weighted Moving Average)だと、現在に最も近い日の重みが一番大きくなり、そこから過去に行くほど線形に(一定量ずつ)重みが減っていきます。

例えば 01-26 の加重移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)から次のように計算します。

現在に最も近い日の 01-26 のデータには 4 を掛けます。次に近い日の 01-25 のデータには 4 から 1 を引いた値を掛けます。その前の日は 4-2、その前の日は 4-3 を掛けます。
それを 4 + 3 + 2 + 1 = 10 で割ります。

01-26 の加重移動平均 = (01-23 * (4-3) + 01-24* (4-2) + 01-25 * (4-1) + 01-26 * (4-0)) / (4 + 3 + 2 + 1)
= (0 * 1 + 0 * 2 + 0 * 3 + 2 * 4) / 10 = 0.8
= (0 + 0 + 0 + 8) / 10 = 0.8

01-27 の加重移動平均には、単純移動平均と同じように 01-26 の加重移動平均を利用します。

01-27 の加重移動平均
= (01-24 * (4-3) + 01-25* (4-2) + 01-26 * (4-1) + 01-27 * (4-0)) / 10
= (- 01-23 - 01-24 - 01-25 - 01-26 + (01-23 * (4-3) + 01-24 * (4-2) + 01-25* (4-1) + 01-26 * (4-0)) + 01-27 * (4-0)) / 10
= (- (01-23 ~ 01-26の総和)/10) + 01-26 の加重移動平均 + 01-27 * 4 / 10
= 01-26 の加重移動平均 - (01-23 ~ 01-26の総和) / 10 + 01-27 * 4 / 10


このように、01-26 の加重移動平均に、01-23 ~ 01-26の総和を10で割った数を引いて、新しい値(01-27)に4を掛けて10で割った値を足せば求めることができます。

01-23 〜 01-25 の加重移動平均は4日間分のデータがないので計算できません。

val newCases3 = newCases2.head(20) val values = newCases3["new_cases_double"].asDoubles() val size = values.size var wma = arrayOfNulls<Double?>(size) var sums = arrayOfNulls<Double?>(size) val n = 4 val n2 = n*(n + 1)/2 // calculate 01-26 var sum = 0.0 var sum_wma = 0.0 for(i in 0 until n) { sum += values[i]!! sum_wma += values[i]!! * (i + 1) } sums[n-1] = sum wma[n-1] = sum_wma/n2 // calculate 01-27 ~ for(i in n until values.size) { sums[i] = sums[i-1]!! - values[i-n]!! + values[i]!! wma[i] = wma[i-1]!! - sums[i-1]!!/n2 + n * values[i]!!/n2 } val newCases5 = newCases4.addColumn("weighted_moving_average") { wma }
赤が単純移動平均、緑が加重移動平均です。


指数移動平均(Exponential Moving Average)

指数移動平均は加重移動平均のように各値に重みをつけたものを足して平均を計算する方法です。各値につける重みが指数関数的に減っていきます。

重みの減少度合いは平滑化係数と呼ばれる0~1の間の値をとる定数 α で決まり、αを時系列区間 N で表した場合 α = 2 / (N+1) となります。
最初の値での EMA は定義しません。2番目の値での EMA をどう設定するかにはいくつかの手法があるそうですが、ここでは単純に2番目の値とします。

3番目以降の場合の EMA の計算式はこうなります。

EMA_t = α * value_t + (1 - α) * EMA_(t-1)



α = 2 / (N + 1) = 2 / (4 + 1) = 0.4 として計算すると
01-24 の指数移動平均 : 0
01-25 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0 = 0
01-26 の指数移動平均 : 0.4 * 2 + (1 - 0.4) * 0 = 0.8
01-27 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0.8 = 0.48


val newCases3 = newCases2.head(20) val values = newCases3["new_cases_double"].asDoubles() val size = values.size var ema = arrayOfNulls<Double?>(size) var sums = arrayOfNulls<Double?>(size) val n = 4 val alpha = 2.0 / (n + 1) ema[0] = null ema[1] = values[1] // calculate 01-25 ~ for(i in 2 until values.size) { ema[i] = alpha * values[i]!! + (1 - alpha) * ema[i - 1]!! } val newCases6 = newCases5.addColumn("exponential_moving_average") { ema }
赤が単純移動平均、緑が加重移動平均、灰色が指数移動平均です。


移動平均は自分で実際に計算してグラフにするとよくわかると思うので、Kotlin じゃなくても、好きな言語でぜひやってみてください。


2020年12月14日月曜日

Jupyter notebooks + Kotlin で移動平均を描画する(その2): krangl で csv データを操作する

このエントリは Fintalk Advent Calendar 2020 の14日目です。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。


krangl は R の dplyr と Python の pandas を参考にした Kotlin のライブラリで、関数スタイルのデータ操作APIがあります。pandas と同じように表形式のデータのフィルタリングや変換、集約などができます。

krangl も Kotlin Jupyter Kernel にバンドルされていて一緒にインストール&セットアップがされているので %use krangl だけで使えます。

ざっくりした使い方は krangl の examples を実際にやってみるのがいいと思います。下に notebook で実行した結果を貼っておきます。



CSVから読み込む時は readCSV() を使います。 val df = DataFrame.readCSV("path/to/file") Covid-19 のデータを読み込むと、数字が入っているカラムに空のところがあるのでDoubleのパースに失敗します。
なので、デフォルトの ColType として String を指定して読み込みます。 val df = DataFrame.readCSV("owid-covid-data.csv", colTypes = mapOf(".default" to ColType.String))
schema() とか head() とか使えます。

日本データだけ出してみましょう。
日付と新規感染者数のデータだけにします。
新規感染者数が空のデータを省きます。
新規感染者数のデータを Double に変換します。
あとは移動平均を計算してグラフにするだけです。それはまた次回〜


2020年12月7日月曜日

Jupyter notebooks + Kotlin で移動平均を描画する(その1): lets-plot-kotlin で線を描画する

このエントリは Fintalk Advent Calendar 2020 の7日目です。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。
Jupyter notebooks + python でやる方法はいっぱい情報があると思うので、ここでは Kotlin でやっていきます。


Kotlin + Jupyter notebooks については去年の Fintalk Advent Calendar のエントリとして書きました。
Kotlin + Jupyter notebooks で給与の額面から手取りを求めてみる

時間が経ってるので、今回の Kotlin + Jupyter notebooks のセットアップ手順を載せておきます。 $ python3 -m venv venv $ source venv/bin/activate (venv) $ pip install --upgrade pip setuptools (venv) $ pip install numpy scipy matplotlib Pillow ipython[all] (venv) $ pip install jupyter 以前の kotlin kernel が残っていたので venv の方をみるように一度削除 (venv) $ jupyter kernelspec list (venv) $ jupyter kernelspec uninstall kotlin (venv) $ pip install kotlin-jupyter-kernel (venv) $ jupyter notebook

グラフの描画には lets-plot-kotlin を使います。
Kotlin Jupyter Kernel にバンドルされていて一緒にインストール&セットアップがされているので %use lets-plot だけで使えます。 lets_plot(data) でプロットを作成し、これにレイヤーを追加していきます。

lets_plot() に渡すデータの型は Map<*, *> です。

例えばこんな感じ。 val data = mapOf( "x" to listOf(0, 1, 2, 3), "y" to listOf(1, 5, 2, 4) ) これは表にするとこういうデータに対応しています。

xy
01
15
22
34


点を描画するレイヤーを追加するには geom_point() を使います。

lambda(この中の this は PointMapping) で x軸、y軸のデータを Map の key 名で指定します。 %use lets-plot val data = mapOf( "x" to listOf(0, 1, 2, 3), "y" to listOf(1, 5, 2, 4) ) lets_plot(data) + geom_point(data, size=5) { x = "x"; y ="y" }


線を描画するレイヤーを追加するには geom_line() を使います。


点と線両方描画したいなら、両方のレイヤーを追加すればOKです。


2020年12月1日火曜日

mutableStateOf() の 3 つの使い方

val mutableState: MutableState<String> = mutableStateOf("") val v = mutableState.value mutableState.value = "1"

destructuring declaration val (value: String, setValue: (String) -> Unit) = mutableStateOf("") val v = value setValue("1")

delegate var value: String by mutableStateOf("") val v = value value = "1"