From 84863f0e2dcec3e68e0f00dd03fe3241962ff346 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 15 Jan 2020 10:18:13 +0100 Subject: [PATCH] Simplify simulation and prepare for #310. (#312) --- .azure-pipelines.yml | 6 +- .../getting_started/tutorial-simulation.ipynb | 22 +- respy/likelihood.py | 9 +- respy/shared.py | 99 +++--- respy/simulate.py | 329 +++++++++--------- respy/tests/test_data_checking.py | 26 -- respy/tests/test_randomness.py | 41 +-- respy/tests/test_replication_kw_94.py | 1 + respy/tests/test_simulate.py | 38 +- 9 files changed, 278 insertions(+), 293 deletions(-) delete mode 100644 respy/tests/test_data_checking.py diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 8fbb95153..f6fe6a3fe 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -28,7 +28,7 @@ jobs: - bash: | source activate respy - tox + tox -- -m "not slow" displayName: Run all tests. - job: @@ -54,7 +54,7 @@ jobs: - script: | call activate respy - tox -e pytest + tox -e pytest -- -m "not slow" displayName: Run pytest. - job: @@ -94,5 +94,5 @@ jobs: - bash: | source activate respy - tox -e pytest + tox -e pytest -- -m "not slow" displayName: Run pytest. diff --git a/docs/getting_started/tutorial-simulation.ipynb b/docs/getting_started/tutorial-simulation.ipynb index d5f83f288..9c1c83f0d 100644 --- a/docs/getting_started/tutorial-simulation.ipynb +++ b/docs/getting_started/tutorial-simulation.ipynb @@ -72,7 +72,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -109,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -118,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -127,16 +127,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" }, @@ -168,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -177,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -186,12 +186,12 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { - "image/png": "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AISEc7CAetTrhv8ZHGZ/q/942sIw+2uybj0o1H8X3bq11n+XkpKS5j//+c+zly9fvjTUY8fBYbMOC/X1KhhfEMrf8VNPPXXQ0qVLW912220brrnmmkPatGlT8fvf/37jVVdddciIESO2n3766dt///vfd7766qu/bNu2bWUYxwxTUb/+ob6u/YuLQnt/mD59esf8/PzWjz/++JpY+8rMzDwsPz+/qFu3buVhxLbXjAlvhvr6Tbp/JO+vAELBCBbQyJSUlDTPzs7Oq94+dOjQvm+//XZ6ImKSIh+ySktL0yQpPT19UKLiQONx/vnnb73ttts2VG+/8847159++unbJemBBx7osmPHjgPKdeXlodYBaEJqem/7z//8z4x77rmnoxQpXlevXt2svmOpmgteeumltieeeGKf+j4mgAgKLABJrbKyUhUVFYkOIxQVFRUaO3Zszz59+uQdd9xx2Tt27LD333+/1cCBA/vl5OTknnTSST8pKytLlSIF8SWXXNJ9yJAhfQ899NC8efPmpZ988sk/6dmz54DJkycfsrfPe++9t8Nhhx3Wv1+/frnnnXdez8ZSHJSUlDTv3bt33jnnnNMzOzs77xe/+EXvv/3tb22POOKIfj179hzw1ltvpU+fPr3juHHjelTf98wzz+w1c+bM9n/4wx86b9q0qdnw4cNzjjrqqBxJOv/883sMGDCgf58+ffKuvvrq71/HzMzMw3772992Gzx4cN+bbrqpa25ubv+9jxUUFLTIy8vrX/04DdG2bdtSRowY0adv37652dnZeQ899FD7efPmpQ8aNKhf3759cw877LD+W7ZsSZGkDRs2NDvhhBOye/bsOWDChAlZe/t44IEHOuTk5ORmZ2fnTZw4MbO2dkjXXXdd2RVXXPGVJD355JOd1qxZU+8FViz27NmT6BCABo0CC2iEysvLdcYZZ/TKycnJHT169KHbt2//wf/1qt+yzpw5s/2ZZ57ZS5LWr1+fdsopp/xkwIAB/QcMGND/1VdfbV3TMbZu3Zryy1/+sldOTk5uTk5O7mOPPXawdGAfsrZu3ZpyzDHH5OTm5vbPycnJffLJJw+WIh+uDz300LwLLrigR15eXu7KlSubx/ByJI01a9a0nDx58qYVK1YsPeiggyoef/zx9hdddFHv2267bd2yZcsK8/Lydl1//fXff+hv3rx5ZX5+fsnFF19cdtZZZ/V56KGH1hQXFy/985//3GnDhg2pixYtajl79uwO+fn5xcXFxYUpKSl+//33d0zkcwzT2rVrW1577bWbiouLl65cubLlU0891TE/P7942rRp66ZNm9attv1vvvnmTZ07d94zb968ZQsWLFgmSXfccccXS5YsKSouLl763nvvtV2wYEGrvdu3bNmycuHChSW33377hrZt21a8//77rSTpgQce6HTeeed9VX/PNH7+8pe/tOvateuekpKSwuXLly8944wztp1//vk/ufPOO9eUlJQUzps3r6RNmzaVklRYWJj+t7/97bOioqKlL774YvsVK1Y0W716dbNbb701c+7cucsKCwuXfvzxx62feOKJg2tqT/TzDdvNN9/c5Q9/+ENnSbrkkku6H3300TmSNGfOnLZjxozpLUlXXnllZt++fXMHDhzYb+3atWmSdM011xwyderULjNnzmy/ZMmS9HHjxh3ar1+/3B07dtg777yTfuSRR/bNy8vrf/zxx2d//vnnNRZfS5YsaXHsscfm9O3bNzc3N7f/0qVLW1RWVupXv/pVVnZ2dl5OTk7uQw891H5fz+Gtt95KHzRoUL/+/fvnDho0qN/ixYtbSJGRtVNPPfXQkSNH9jnhhBNywnrNgKaIAgtohFavXt1ywoQJZcuWLSts27Zt5X/9139l7M9+v/rVr7pfc801G5csWVL017/+deWECRN61bTtDTfc0K1du3YVy5YtK1y2bFnhaaedtv1AP2Slp6dX/s///M+KwsLConnz5i278cYbsyorK79/DhdffPFXRUVFhTk5Od8d6GuQjDIzM3cfe+yxuyRp0KBBO1euXNli+/btqaeddtoOSbrsssu+mj9/fpu92//bv/3b15I0cODAXX369NnVs2fPPa1atfLu3bvv/uyzz5q/8sorbZcsWZI+cODA/v369ct9991323322WctEvPswpeZmbl76NChu1JTU5WTk7Nr5MiR21JSUnTEEUfsXLduXZ2e56xZszrk5ub2z83NzV2+fHnLxYsXt9z72Lhx47bsvX3RRRd9+dBDD3UqLy/XnDlz2l9yySWNosA64ogjdr3zzjvtJk6cmPnKK6+0WblyZfPOnTvvGT58+E5J6tChQ2WzZpHP98cff/y2jh07VqSnp3ufPn2+XblyZYt333239dFHH739kEMOKW/WrJnOOeeczfPmzWtTU3tCn2w9OPHEE3e89957bSTpk08+Sf/mm29Sd+/ebW+//Xab448/fvuuXbtSjjnmmB0lJSWFxxxzzI677777B++9F1988ZYBAwbsfPzxxz8rLi4ubNasmSZPntxjzpw5K5cuXVo0fvz4L3/729/W+MXUeeed13vChAmbSkpKCvPz84t79Oix5/HHHz+4oKCgVVFR0dI33nhj2dSpU7P2VaQNHDjw2w8//LC4qKio8JZbbvniuuuu+350ctGiRW2eeeaZVfPnz18WxusFNFUscgE0Ql27dv3u5JNP/kaSLrzwwq+mT5/eeX/2e++999otX778+2/0d+zYkbply5aU9u3b/2iBgLfffrvds88++9ne+xkZGRX/+Mc/2u79kCXp+w9ZF1544dfRjldZWWlXXXVV1vz589ukpKRo06ZNzdetW5cmSd26dftu1KhR3xzYM09uzZs3/34J/9TUVP/666/3OU2oZcuWLkkpKSlq0aLF9/umpKSovLzc3N3OOuusr2bMmPFF/UWdOFVfr5SUlO9fj9TUVFVUVNiB9ldcXNz8nnvu6bJw4cKijIyMijPPPLPXt99++/0XjVUXwhg/fvyW22+//ZBnn312+2GHHbaza9eujWKe6uGHH7570aJFhS+88MJBN910U+aJJ564zcyiXlqi+t/rnj17zD36VShqam9sjj/++J3jx49vvWXLlpQWLVr44YcfvuOdd95J/+CDD9refffda5o1a+Zjx47dKkmDBw/+5vXXX2+3r/4+/fTTFsuXL281cuTIHCkyJTojIyPq/LwtW7akbNy4sfm4ceO+lqT09HSX5O+8807bs88+e3NaWpq6d+9eftRRR+14991304cMGbIrWj+bN29OPeecc3qvXr26pZn5nj17vv+/dMIJJ2zr0qVLo/hbBxKJESygETKz/b6/a9eu7++4u/Lz84uKi4sLi4uLCzdt2vRptOJq77bV+z3QD1kPPPBAh6+++iqtoKCgqLi4uLBjx457du3alSJFRrcOqLMG6KCDDqpo165dxSuvvNJGkh555JGOxxxzzI793X/06NHbXnrppfZffPFFmiRt3LgxddmyZY1iOmVYWrduXbF169YUSdqyZUtqq1atKjt06FCxdu3atLlz5x5U037p6ek+fPjwrddcc02Piy666Mv4RVy/Vq9e3axt27aVv/71rzdfddVVGz/66KPWGzdubD5v3rx0KfIhfl/n3wwbNuybBQsWtC0tLU0rLy/X888/32HEiBE7amqP2xOLkxYtWnhWVtbuGTNmdBo6dOiOYcOG7Xj99dfbfv755y0GDRr0bVpamqekRD5apaWlqby8fJ9fBLi79enTZ9fe99xly5YVvvfee8tr2LamPg7oOVx//fWZw4cP3758+fKlf//731d89913338WbArvu0A8MIIF1KNELateWlra/PXXX2/9s5/97Junn366w7HHHrvj5Zdf/n6qXseOHfcsWrSo5cCBA7+dM2dO+zZt2lRIkSlBt99+e+f/+I//2ChJ77//fqu9U9qqGzFixLY77rij86OPPrpWksrKylKHDRv2zfXXX9+9tLQ0LSMjo/z555/v8Otf/3pTTXFu3bo1tVOnTntatGjhf//739uuX78+PsXBfiyrHi8zZ85cNXHixJ6TJ09O6dGjx+5nnnlm9f7uO3jw4G9vvvnmL0aNGpVTWVmpZs2a+fTp09eEPaUyrGXVE2H8+PFfnnrqqdmdO3fes2DBgmUDBgzYmZ2dndejR4/dgwcP3mcBMG7cuM0vv/xy+zPOOGNbfcQW5rLq+2vhwoWtpkyZkpWSkqK0tDS/9957P3d3TZ48uce3336b0rJly8q33367xulhPXv23DN16tQvhg8fnuPuNmrUqK0XXHDB15JUU3t9SdSy6scee+yOGTNmdLnvvvtWDx48eNeNN96YNWDAgJ17C6vatGnTpmLr1q2pknT44Yd/u3nz5rS979e7d++2goKCFkOGDPm2+n4dOnSo7Nq163dPPPHEwRdeeOHXu3btsvLychs+fPj2hx56KOOKK674atOmTWkffvhhm+nTp6/d+2VVddu2bUvNysr6ToqcXxjDSwGgBjUO9wM4cIsXL149cODAhH7bXVJS0vxf/uVfso866qjt+fn5bXr37r179uzZq0aNGpX9pz/9ae2wYcN2zpw5s/3UqVMzu3Xrtqdfv367vvnmm5QXXnhhdWlpadqll17aY/ny5S0rKirsqKOO2v70009HvQ7O1q1bUy6++OIeBQUFrVNSUvzGG29cP378+K/vv//+DnfccUfXvR+y7r///nXSD6+Fk56ePmjnzp0fl5aWpp166ql9ysvLLS8vb+dHH33U5uWXX14uSQ31mlFoPKZOndpl69atqXfdddf6RMeC5DFnzpy2Z555ZvbmzZs/adeuXWWvXr0GXHTRRWW33nrrxr3vbVJkAaGXXnrpoBdeeGF11eu0PfbYYwffeuutWS1btqzMz88v+vTTT1tOnjy5x/bt21MrKips4sSJG6+99tqoeaSgoKDFZZdd1nPz5s1pzZo18+eff35lv379vps4cWLWm2++eZCZ+b//+7+XXnbZZVuqXnvvpZdeavvf//3fXd56660Vr7/+eutLL720d4cOHcpPOOGEbbNnz+74xRdfFNR27bPFixd3GjhwYK96fGmBRoMCCwhRMhRYAGJ30kkn/eTzzz9vMW/evGVhXyAXaIgosID9xxRBAACqee2111YmOgYAQMNEgQVgn+66666O9913X5eqbUceeeSOJ554Iuo0EgBAbC688MIeH3300Q+WuZ84ceLG3/zmN43icgFAY8cUQSBEixcv/uywww7bkpKSwn8sAECjUFlZaQUFBe0HDhx4aKJjARoClmkHwrWkrKzsoMrKygO+Rg8AAMmmsrLSysrKDpK0JNGxAA0FUwSBEJWXl1+6YcOGhzds2DBAfIEBAGj4KiUtKS8vvzTRgQANBVMEAQAAACAkfMMOAAAAACGhwAIAAACAkFBgAQAAAEBI/j9vmeILQfGuMgAAAABJRU5ErkJggg==\n", + "image/png": "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AAAAQsI1WEA96nXDPwaH2V/pH09fEGZ/TdatB4X696JbN9f691JSUtL8pz/9afayZcuWhHrsODhsxmGhfl4F4wpC+Xf89NNPH7RkyZJWt91227prrrnmkDZt2lT+/ve/X3/VVVcdcuKJJ24944wztv7+97/vfPXVV3/Ztm3bqjCOGaaifv1D/Vz7FxeF9vth2rRpHfPz81s/8cQTq2LtKzMz87D8/Pyibt26VYQR2x73jn8r1M9v4gMj+P0KIBSMYAGNTElJSfPs7Oy8vduHDh3ad+7cuemJiEmKfMkqKytLk6T09PRBiYoDjccFF1yw+bbbblu3d/udd9659owzztgqSQ8++GCXbdu2HVCuq6gItQ5AE1LT77b/+q//yrjnnns6SpHitbS0tFl9x1I9F7z88sttTzrppD71fUwAERRYAJJaVVWVKisrEx1GKCorKzVmzJieffr0yTvuuOOyt23bZu+//36rgQMH9svJyck9+eSTf1ReXp4qRQriSy+9tPuQIUP6HnrooXlz5sxJP+WUU37Us2fPAZMmTTpkT5/33Xdfh8MOO6x/v379cs8///yejaU4KCkpad67d++8c889t2d2dnbez372s95/+9vf2h5xxBH9evbsOeDtt99OnzZtWsexY8f22Hvfs846q9f06dPb/+EPf+i8YcOGZsOHD8856qijciTpggsu6DFgwID+ffr0ybv66qu/+xwzMzMP++1vf9tt8ODBfW+66aauubm5/fe8VlBQ0CIvL6//3sdpiLZs2ZJy4okn9unbt29udnZ23sMPP9x+zpw56YMGDerXt2/f3MMOO6z/pk2bUiRp3bp1zU444YTsnj17Dhg/fnzWnj4efPDBDjk5ObnZ2dl5EyZMyKytHdJ1111XfuWVV34lSU899VSnVatW1XuBFYvdu3cnOgSgQaPAAhqhiooKnXnmmb1ycnJyR40adejWrVu/93+9+lnW6dOntz/rrLN6SdLatWvTTj311B8NGDCg/4ABA/q/9tprrWs6xubNm1N+8Ytf9MrJycnNycnJffzxxw+WDuxL1ubNm1OOOeaYnNzc3P45OTm5Tz311MFS5Mv1oYcemnfhhRf2yMvLy12xYkXzGD6OpLFq1aqWkyZN2rB8+fIlBx10UOUTTzzR/uKLL+592223rVm6dGlhXl7ejuuvv/67L/3Nmzevys/PL7nkkkvKzz777D4PP/zwquLi4iV//vOfO61bty514cKFLWfOnNkhPz+/uLi4uDAlJcUfeOCBjol8j2FavXp1y2uvvXZDcXHxkhUrVrR8+umnO+bn5xdPnTp1zdSpU7vVtv/NN9+8oXPnzrvnzJmzdP78+Usl6Y477vhi8eLFRcXFxUvee++9tvPnz2+1Z/uWLVtWLViwoOT2229f17Zt28r333+/lSQ9+OCDnc4///yv6u+dxs9f/vKXdl27dt1dUlJSuGzZsiVnnnnmlgsuuOBHd95556qSkpLCOXPmlLRp06ZKkgoLC9P/9re/fVZUVLTkpZdear98+fJmpaWlzW699dbM2bNnLy0sLFzy8ccft37yyScPrqk90e83bDfffHOXP/zhD50l6dJLL+1+9NFH50jSrFmz2o4ePbq3JP3617/O7Nu3b+7AgQP7rV69Ok2SrrnmmkOmTJnSZfr06e0XL16cPnbs2EP79euXu23bNnvnnXfSjzzyyL55eXn9jz/++OzPP/+8xuJr8eLFLY499ticvn375ubm5vZfsmRJi6qqKv3yl7/Mys7OzsvJycl9+OGH2+/rPbz99tvpgwYN6te/f//cQYMG9Vu0aFELKTKydtpppx06YsSIPieccEJOWJ8Z0BRRYAGNUGlpacvx48eXL126tLBt27ZV//3f/52xP/v98pe/7H7NNdesX7x4cdFf//rXFePHj+9V07Y33HBDt3bt2lUuXbq0cOnSpYWnn3761gP9kpWenl71j3/8Y3lhYWHRnDlzlt54441ZVVVV372HSy655KuioqLCnJycbw/0M0hGmZmZu4499tgdkjRo0KDtK1asaLF169bU008/fZskXX755V/NmzevzZ7tf/7zn38tSQMHDtzRp0+fHT179tzdqlUr7969+67PPvus+auvvtp28eLF6QMHDuzfr1+/3HfffbfdZ5991iIx7y58mZmZu4YOHbojNTVVOTk5O0aMGLElJSVFRxxxxPY1a9bU6X3OmDGjQ25ubv/c3NzcZcuWtVy0aFHLPa+NHTt2057HF1988ZcPP/xwp4qKCs2aNav9pZde2igKrCOOOGLHO++8027ChAmZr776apsVK1Y079y58+7hw4dvl6QOHTpUNWsW+X5//PHHb+nYsWNlenq69+nTZ+eKFStavPvuu62PPvrorYccckhFs2bNdO65526cM2dOm5raE/pm68FJJ5207b333msjSZ988kn6N998k7pr1y6bO3dum+OPP37rjh07Uo455phtJSUlhcccc8y2u++++3u/ey+55JJNAwYM2P7EE098VlxcXNisWTNNmjSpx6xZs1YsWbKkaNy4cV/+9re/rfHE1Pnnn997/PjxG0pKSgrz8/OLe/TosfuJJ544uKCgoFVRUdGSN998c+mUKVOy9lWkDRw4cOeHH35YXFRUVHjLLbd8cd111303Orlw4cI2zz777Mp58+YtDePzApoqFrkAGqGuXbt+e8opp3wjSRdddNFX06ZN67w/+7333nvtli1b9t0Z/W3btqVu2rQppX379j9YIGDu3Lntnnvuuc/2PM/IyKj85z//2XbPlyxJ333Juuiii76Odryqqiq76qqrsubNm9cmJSVFGzZsaL5mzZo0SerWrdu3I0eO/ObA3nlya968+XdL+KempvrXX3+9z2lCLVu2dElKSUlRixYtvts3JSVFFRUV5u529tlnf3Xvvfd+UX9RJ071zyslJeW7zyM1NVWVlZV2oP0VFxc3v+eee7osWLCgKCMjo/Kss87qtXPnzu9ONFZfCGPcuHGbbr/99kOee+65rYcddtj2rl27Nop5qocffviuhQsXFr744osH3XTTTZknnXTSFjOLemuJvf+97t6929yj34WipvbG5vjjj98+bty41ps2bUpp0aKFH3744dveeeed9A8++KDt3XffvapZs2Y+ZsyYzZI0ePDgb9544412++rv008/bbFs2bJWI0aMyJEiU6IzMjKizs/btGlTyvr165uPHTv2a0lKT093Sf7OO++0PeecczampaWpe/fuFUcdddS2d999N33IkCE7ovWzcePG1HPPPbd3aWlpSzPz3bt3f/d/6YQTTtjSpUuXRvFvHUgkRrCARsjM9vv5jh07vnvi7srPzy8qLi4uLC4uLtywYcOn0YqrPdvu3e+Bfsl68MEHO3z11VdpBQUFRcXFxYUdO3bcvWPHjhQpMrp1QJ01QAcddFBlu3btKl999dU2kvToo492POaYY7bt7/6jRo3a8vLLL7f/4osv0iRp/fr1qUuXLm0U0ynD0rp168rNmzenSNKmTZtSW7VqVdWhQ4fK1atXp82ePfugmvZLT0/34cOHb77mmmt6XHzxxV/GL+L6VVpa2qxt27ZVv/rVrzZeddVV6z/66KPW69evbz5nzpx0KfIlfl/X3wwbNuyb+fPnty0rK0urqKjQCy+80OHEE0/cVlN73N5YnLRo0cKzsrJ23XvvvZ2GDh26bdiwYdveeOONtp9//nmLQYMG7UxLS/OUlMhXq7S0NFVUVOzzRIC7W58+fXbs+Z27dOnSwvfee29ZDdvW1McBvYfrr78+c/jw4VuXLVu25O9///vyb7/99rvvgk3h9y4QD4xgAfUoUcuql5WVNX/jjTda/+QnP/nmmWee6XDsscdue+WVV76bqtexY8fdCxcubDlw4MCds2bNat+mTZtKKTIl6Pbbb+/8n//5n+sl6f3332+1Z0rb3k488cQtd9xxR+fHHntstSSVl5enDhs27Jvrr7++e1lZWVpGRkbFCy+80OFXv/rVhpri3Lx5c2qnTp12t2jRwv/+97+3Xbt2bXyKg/1YVj1epk+fvnLChAk9J02alNKjR49dzz77bOn+7jt48OCdN9988xcjR47MqaqqUrNmzXzatGmrwp5SGday6okwbty4L0877bTszp07754/f/7SAQMGbM/Ozs7r0aPHrsGDB++zABg7duzGV155pf2ZZ565pT5iC3NZ9f21YMGCVpMnT85KSUlRWlqa33fffZ+7uyZNmtRj586dKS1btqyaO3dujdPDevbsuXvKlClfDB8+PMfdbeTIkZsvvPDCryWppvb6kqhl1Y899tht9957b5f777+/dPDgwTtuvPHGrAEDBmzfU1jVpk2bNpWbN29OlaTDDz9858aNG9P2/L7etWuXFRQUtBgyZMjOvffr0KFDVdeuXb998sknD77ooou+3rFjh1VUVNjw4cO3PvzwwxlXXnnlVxs2bEj78MMP20ybNm31npNVe9uyZUtqVlbWt1Lk+sIYPgoANahxuB/AgVu0aFHpwIEDE3q2u6SkpPm//du/ZR911FFb8/Pz2/Tu3XvXzJkzV44cOTL7T3/60+phw4Ztnz59evspU6ZkduvWbXe/fv12fPPNNykvvvhiaVlZWdpll13WY9myZS0rKyvtqKOO2vrMM89EvQ/O5s2bUy655JIeBQUFrVNSUvzGG29cO27cuK8feOCBDnfccUfXPV+yHnjggTXS9++Fk56ePmj79u0fl5WVpZ122ml9KioqLC8vb/tHH33U5pVXXlkmSQ31nlFoPKZMmdJl8+bNqXfdddfaRMeC5DFr1qy2Z511VvbGjRs/adeuXVWvXr0GXHzxxeW33nrr+j2/26TIAkIvv/zyQS+++GJp9fu0Pf744wffeuutWS1btqzKz88v+vTTT1tOmjSpx9atW1MrKyttwoQJ66+99tqoeaSgoKDF5Zdf3nPjxo1pzZo18xdeeGFFv379vp0wYULWW2+9dZCZ+X/8x3+UXX755Zuq33vv5Zdfbvs///M/Xd5+++3lb7zxRuvLLrusd4cOHSpOOOGELTNnzuz4xRdfFNR277NFixZ1GjhwYK96/GiBRoMCCwhRMhRYAGJ38skn/+jzzz9vMWfOnKVh3yAXaIgosID9xxRBAAD28vrrr69IdAwAgIaJAgvAPt11110d77///i7V24488shtTz75ZNRpJACA2Fx00UU9Pvroo+8tcz9hwoT1v/nNbxrF7QKAxo4pgkCIFi1a9Nlhhx22KSUlhf9YAIBGoaqqygoKCtoPHDjw0ETHAjQELNMOhGtxeXn5QVVVVQd8jx4AAJJNVVWVlZeXHyRpcaJjARoKpggCIaqoqLhs3bp1j6xbt26AOIEBAGj4qiQtrqiouCzRgQANBVMEAQAAACAknGEHAAAAgJBQYAEAAABASCiwAAAAACAk/x/K9viboGCNOAAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -242,7 +242,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.7.6" } }, "nbformat": 4, diff --git a/respy/likelihood.py b/respy/likelihood.py index 917a287a1..6e79c2353 100644 --- a/respy/likelihood.py +++ b/respy/likelihood.py @@ -18,7 +18,7 @@ from respy.shared import create_base_draws from respy.shared import downcast_to_smallest_dtype from respy.shared import generate_column_dtype_dict_for_estimation -from respy.shared import rename_labels +from respy.shared import rename_labels_to_internal from respy.solve import solve_with_backward_induction from respy.state_space import StateSpace @@ -499,8 +499,11 @@ def _process_estimation_data(df, state_space, optim_paras, options): """ col_dtype = generate_column_dtype_dict_for_estimation(optim_paras) - df = df.sort_index()[list(col_dtype)[2:]] - df = df.rename(columns=rename_labels).rename_axis(index=rename_labels) + df = ( + df.sort_index()[list(col_dtype)[2:]] + .rename(columns=rename_labels_to_internal) + .rename_axis(index=rename_labels_to_internal) + ) df = convert_labeled_variables_to_codes(df, optim_paras) # Get indices of states in the state space corresponding to all observations for all diff --git a/respy/shared.py b/respy/shared.py index ec5906869..71dfc590d 100644 --- a/respy/shared.py +++ b/respy/shared.py @@ -4,6 +4,8 @@ import from respy itself. This is to prevent circular imports. """ +import copy + import chaospy as cp import numba as nb import numpy as np @@ -107,8 +109,8 @@ def create_base_draws(shape, seed, monte_carlo_sequence): University Press.* """ - n_choices = shape[2] - n_points = shape[0] * shape[1] + n_choices = shape[-1] + n_points = np.prod(shape[:-1]) np.random.seed(seed) @@ -151,8 +153,8 @@ def transform_base_draws_with_cholesky_factor(draws, shocks_cholesky, n_wages): """ draws_transformed = draws.dot(shocks_cholesky.T) - draws_transformed[:, :, :n_wages] = np.exp( - np.clip(draws_transformed[:, :, :n_wages], MIN_LOG_FLOAT, MAX_LOG_FLOAT) + draws_transformed[..., :n_wages] = np.exp( + np.clip(draws_transformed[..., :n_wages], MIN_LOG_FLOAT, MAX_LOG_FLOAT) ) return draws_transformed @@ -179,25 +181,6 @@ def generate_column_dtype_dict_for_estimation(optim_paras): return column_dtype_dict -def generate_column_dtype_dict_for_simulation(optim_paras): - """Generate column labels for simulation output.""" - est_col_dtype = generate_column_dtype_dict_for_estimation(optim_paras) - labels = ["Type"] if optim_paras["n_types"] >= 2 else [] - labels += ( - [f"Nonpecuniary_Reward_{choice.title()}" for choice in optim_paras["choices"]] - + [f"Wage_{choice.title()}" for choice in optim_paras["choices_w_wage"]] - + [f"Flow_Utility_{choice.title()}" for choice in optim_paras["choices"]] - + [f"Value_Function_{choice.title()}" for choice in optim_paras["choices"]] - + [f"Shock_Reward_{choice.title()}" for choice in optim_paras["choices"]] - + ["Discount_Rate"] - ) - - sim_col_dtype = {col: (int if col == "Type" else float) for col in labels} - sim_col_dtype = {**est_col_dtype, **sim_col_dtype} - - return sim_col_dtype - - @nb.njit def clip(x, minimum=None, maximum=None): """Clip input array at minimum and maximum.""" @@ -214,37 +197,48 @@ def clip(x, minimum=None, maximum=None): return out -def downcast_to_smallest_dtype(series): +def downcast_to_smallest_dtype(series, downcast_options=None): """Downcast the dtype of a :class:`pandas.Series` to the lowest possible dtype. + By default, variables are converted to signed or unsigned integers. Use ``"float"`` + to cast variables from ``float64`` to ``float32``. + Be aware that NumPy integers silently overflow which is why conversion to low dtypes - should be done after calculations. For example, using :class:`np.uint8` for an array - and squaring the elements leads to silent overflows for numbers higher than 255. + should be done after calculations. For example, using :class:`numpy.uint8` for an + array and squaring the elements leads to silent overflows for numbers higher than + 255. - For more information on the boundaries the NumPy documentation under + For more information on the dtype boundaries see the NumPy documentation under https://docs.scipy.org/doc/numpy-1.17.0/user/basics.types.html. """ # We can skip integer as "unsigned" and "signed" will find the same dtypes. - _downcast_options = ["unsigned", "signed", "float"] + if downcast_options is None: + downcast_options = ["unsigned", "signed"] if series.dtype.name == "category": out = series + # Convert bools to integers because they turn the dot product in + # `create_choice_rewards` to the object dtype. elif series.dtype == np.bool: out = series.astype(np.dtype("uint8")) else: - min_dtype = np.dtype("float64") + min_dtype = series.dtype - for dc_opt in _downcast_options: - dtype = pd.to_numeric(series, downcast=dc_opt).dtype + for dc_opt in downcast_options: + try: + dtype = pd.to_numeric(series, downcast=dc_opt).dtype + # A ValueError happens if strings are found in the series. + except ValueError: + min_dtype = "category" + break - if dtype.itemsize == 1 and dtype.name.startswith("u"): + # If we can convert the series to an unsigned integer, we can stop. + if dtype.name.startswith("u"): min_dtype = dtype break - elif dtype.itemsize == min_dtype.itemsize and dtype.name.startswith("u"): - min_dtype = dtype elif dtype.itemsize < min_dtype.itemsize: min_dtype = dtype else: @@ -282,15 +276,29 @@ def create_base_covariates(states, covariates_spec, raise_errors=True): """ covariates = states.copy() - for covariate, definition in covariates_spec.items(): - if covariate not in states.columns: - try: - covariates[covariate] = covariates.eval(definition) - except pd.core.computation.ops.UndefinedVariableError as e: - if raise_errors: - raise e - else: + has_covariates_left_changed = True + covariates_left = list(covariates_spec) + + while has_covariates_left_changed: + n_covariates_left = len(covariates_left) + + # Create a copy of `covariates_left` to remove elements without side-effects. + for covariate in copy.copy(covariates_left): + # Check if the covariate does not exist and needs to be computed. + is_covariate_missing = covariate not in covariates.columns + + if is_covariate_missing: + try: + covariates[covariate] = covariates.eval(covariates_spec[covariate]) + except pd.core.computation.ops.UndefinedVariableError: pass + else: + covariates_left.remove(covariate) + + has_covariates_left_changed = n_covariates_left != len(covariates_left) + + if covariates_left and raise_errors: + raise Exception(f"Cannot compute all covariates: {covariates_left}.") covariates = covariates.drop(columns=states.columns) @@ -323,11 +331,16 @@ def convert_labeled_variables_to_codes(df, optim_paras): return df -def rename_labels(x): +def rename_labels_to_internal(x): """Shorten labels and convert them to lower-case.""" return x.replace("Experience", "exp").lower() +def rename_labels_from_internal(x): + """Shorten labels and convert them to lower-case.""" + return x.replace("exp", "Experience").title() + + def normalize_probabilities(probabilities): """Normalize probabilities such that their sum equals one. diff --git a/respy/simulate.py b/respy/simulate.py index cf1c8f75e..79cf6b91c 100644 --- a/respy/simulate.py +++ b/respy/simulate.py @@ -10,8 +10,9 @@ from respy.shared import calculate_value_functions_and_flow_utilities from respy.shared import create_base_covariates from respy.shared import create_base_draws -from respy.shared import generate_column_dtype_dict_for_simulation -from respy.shared import rename_labels +from respy.shared import downcast_to_smallest_dtype +from respy.shared import rename_labels_from_internal +from respy.shared import rename_labels_to_internal from respy.shared import transform_base_draws_with_cholesky_factor from respy.solve import solve_with_backward_induction from respy.state_space import StateSpace @@ -54,19 +55,17 @@ def get_simulate_func( """ optim_paras, options = process_params_and_options(params, options) - df, n_simulation_periods, options = _harmonize_simulation_arguments( + n_simulation_periods, options = _harmonize_simulation_arguments( method, df, n_simulation_periods, options ) - df = _process_input_df_for_simulation(df, method, options, optim_paras) + df = _process_input_df_for_simulation( + df, method, n_simulation_periods, options, optim_paras + ) state_space = StateSpace(optim_paras, options) - shape = ( - n_simulation_periods, - options["simulation_agents"], - len(optim_paras["choices"]), - ) + shape = (df.shape[0], len(optim_paras["choices"])) base_draws_sim = create_base_draws( shape, next(options["simulation_seed_startup"]), "random" ) @@ -79,7 +78,6 @@ def get_simulate_func( base_draws_sim=base_draws_sim, base_draws_wage=base_draws_wage, df=df, - n_simulation_periods=n_simulation_periods, state_space=state_space, options=options, ) @@ -87,15 +85,7 @@ def get_simulate_func( return simulate_function -def simulate( - params, - base_draws_sim, - base_draws_wage, - df, - n_simulation_periods, - state_space, - options, -): +def simulate(params, base_draws_sim, base_draws_wage, df, state_space, options): """Perform a simulation. This function performs one of three possible simulation exercises. The type of the @@ -135,10 +125,6 @@ def simulate( a one-step-ahead simulation. - :class:`pandas.DataFrame` containing only first observations which triggers a n-step-ahead simulation taking the data as initial conditions. - n_simulation_periods : int - Simulate data for a number of periods. This options does not affect - ``options["n_periods"]`` which controls the number of periods for which decision - rules are computed. state_space : :class:`~respy.state_space.StateSpace` State space of the model. options : dict @@ -150,6 +136,7 @@ def simulate( DataFrame of simulated individuals. """ + # Copy DataFrame so that the DataFrame attached to :func:`simulate` is not altered. df = df.copy() optim_paras, options = process_params_and_options(params, options) @@ -159,43 +146,47 @@ def simulate( state_space = solve_with_backward_induction(state_space, optim_paras, options) # Prepare simulation. + n_simulation_periods = int(df.index.get_level_values("period").max() + 1) + + # Prepare shocks. n_wages = len(optim_paras["choices_w_wage"]) base_draws_sim_transformed = transform_base_draws_with_cholesky_factor( base_draws_sim, optim_paras["shocks_cholesky"], n_wages ) base_draws_wage_transformed = np.exp(base_draws_wage * optim_paras["meas_error"]) - df = df.copy() + # Store the shocks inside the DataFrame. The sorting ensures that regression tests + # still work. + df = df.sort_index(level=["period", "identifier"]) + for i, choice in enumerate(optim_paras["choices"]): + df[f"shock_reward_{choice}"] = base_draws_sim_transformed[:, i] + df[f"meas_error_wage_{choice}"] = base_draws_wage_transformed[:, i] + df = df.sort_index(level=["identifier", "period"]) + df = _extend_data_with_sampled_characteristics(df, optim_paras, options) - is_n_step_ahead = df.index.get_level_values("identifier").duplicated().sum() == 0 - # Start simulating. - data = [] + core_columns = create_core_state_space_columns(optim_paras) + is_n_step_ahead = np.any(df[core_columns].isna()) + for period in range(n_simulation_periods): # If it is a one-step-ahead simulation, we pick rows from the panel data. For - # n-step-ahead simulation, ``df`` always contains only data of the current - # period. - current_states = df.query("period == @period").to_numpy(dtype=np.uint32) - - rows = _simulate_single_period( - period, - current_states, - state_space, - base_draws_sim_transformed, - base_draws_wage_transformed, - optim_paras, + # n-step-ahead simulation, `df` always contains only data of the current period. + current_df = df.query("period == @period").copy() + + current_df_extended = _simulate_single_period( + current_df, state_space, optim_paras ) - data.append(rows) + # Add all columns with simulated information to the complete DataFrame. + df = df.reindex(columns=current_df_extended.columns) if period == 0 else df + df = df.combine_first(current_df_extended) - if is_n_step_ahead: - choices = rows[:, 1].astype(np.uint8) - df = _apply_law_of_motion(df, choices, optim_paras) + if is_n_step_ahead and period != n_simulation_periods - 1: + next_df = _apply_law_of_motion(current_df_extended, optim_paras) + df = df.combine_first(next_df) - simulated_data = _create_simulated_data( - data, df, is_n_step_ahead, n_simulation_periods, optim_paras, options - ) + simulated_data = _process_simulation_output(df, optim_paras) return simulated_data @@ -226,47 +217,50 @@ def _extend_data_with_sampled_characteristics(df, optim_paras, options): A pandas DataFrame with no missings at all. """ - index = df.index + # Sample characteristics only for the first period. + fp = df.query("period == 0").copy() + index = fp.index for observable in optim_paras["observables"]: level_dict = optim_paras["observables"][observable] - sampled_char = _sample_characteristic(df, options, level_dict, use_keys=False) - df[observable] = df[observable].fillna( + sampled_char = _sample_characteristic(fp, options, level_dict, use_keys=False) + fp[observable] = fp[observable].fillna( pd.Series(data=sampled_char, index=index), downcast="infer" ) for choice in optim_paras["choices_w_exp"]: level_dict = optim_paras["choices"][choice]["start"] - sampled_char = _sample_characteristic(df, options, level_dict, use_keys=True) - df[f"exp_{choice}"] = df[f"exp_{choice}"].fillna( + sampled_char = _sample_characteristic(fp, options, level_dict, use_keys=True) + fp[f"exp_{choice}"] = fp[f"exp_{choice}"].fillna( pd.Series(data=sampled_char, index=index), downcast="infer" ) for lag in reversed(range(1, optim_paras["n_lagged_choices"] + 1)): level_dict = optim_paras[f"lagged_choice_{lag}"] - sampled_char = _sample_characteristic(df, options, level_dict, use_keys=False) - df[f"lagged_choice_{lag}"] = df[f"lagged_choice_{lag}"].fillna( + sampled_char = _sample_characteristic(fp, options, level_dict, use_keys=False) + fp[f"lagged_choice_{lag}"] = fp[f"lagged_choice_{lag}"].fillna( pd.Series(data=sampled_char, index=index), downcast="infer" ) + # Sample types and map them to individuals for all periods. if optim_paras["n_types"] >= 2: level_dict = optim_paras["type_prob"] - types = _sample_characteristic(df, options, level_dict, use_keys=False) - df["type"] = df["type"].fillna( + types = _sample_characteristic(fp, options, level_dict, use_keys=False) + fp["type"] = fp["type"].fillna( pd.Series(data=types, index=index), downcast="infer" ) + # Update data in the first period with sampled characteristics. + df = df.combine_first(fp) + + # Types are invariant and we have to fill the DataFrame for one-step-ahead. + if optim_paras["n_types"] >= 2: + df["type"] = df["type"].fillna(method="ffill") + return df -def _simulate_single_period( - period, - current_states, - state_space, - base_draws_sim_transformed, - base_draws_wage_transformed, - optim_paras, -): +def _simulate_single_period(df, state_space, optim_paras): """Simulate individuals in a single period. This function takes a set of states and simulates wages, choices and other @@ -274,27 +268,20 @@ def _simulate_single_period( Parameter --------- - period : int - The period for which individual outcomes are simulated. - current_states : numpy.ndarray - Array with shape (n_individuals, n_state_space_dims) which contains the states - of simulated individuals. + df : pandas.DataFrame + DataFrame with shape (n_individuals_in_period, n_state_space_dims) which + contains the states of simulated individuals. state_space : :class:`~respy.state_space.StateSpace` State space of the model. - base_draws_sim_transformed : numpy.ndarray - Draws to simulate choices of individuals. - base_draws_wage_transformed : numpy.ndarray - Draws to simulate the measurement error in wages. optim_paras : dict """ + period = df.index.get_level_values("period").max() n_wages = len(optim_paras["choices_w_wage"]) - n_individuals = current_states.shape[0] # Get indices which connect states in the state space and simulated agents. - indices = state_space.indexer[period][ - tuple(current_states[:, i] for i in range(current_states.shape[1])) - ] + columns = create_state_space_columns(optim_paras) + indices = state_space.indexer[period][tuple(df[col].astype(int) for col in columns)] # Get continuation values. Indices work on the complete state space whereas # continuation values are period-specific. Make them period-specific. @@ -302,8 +289,8 @@ def _simulate_single_period( cont_indices = indices - state_space.slices_by_periods[period].start # Select relevant subset of random draws. - draws_shock = base_draws_sim_transformed[period][:n_individuals] - draws_wage = base_draws_wage_transformed[period][:n_individuals] + draws_shock = df[[f"shock_reward_{c}" for c in optim_paras["choices"]]].to_numpy() + draws_wage = df[[f"meas_error_wage_{c}" for c in optim_paras["choices"]]].to_numpy() # Get total values and ex post rewards. value_functions, flow_utilities = calculate_value_functions_and_flow_utilities( @@ -329,29 +316,18 @@ def _simulate_single_period( wages[:, n_wages:] = np.nan wage = np.choose(choice, wages.T) - rows = np.column_stack( - ( - np.full(n_individuals, period), - choice, - wage, - # Write relevant state space for period to data frame. However, the - # individual's type is not part of the observed dataset. This is included in - # the simulated dataset. - current_states, - # As we are working with a simulated dataset, we can also output additional - # information that is not available in an observed dataset. The discount - # rate is included as this allows to construct the EMAX with the information - # provided in the simulation output. - state_space.nonpec[indices], - state_space.wages[indices, :n_wages], - flow_utilities, - value_functions, - draws_shock, - np.full(n_individuals, optim_paras["delta"]), - ) - ) + # Store necessary information and information for debugging, etc.. + df["choice"] = choice + df["wage"] = wage + df["discount_rate"] = optim_paras["delta"] + for i, choice in enumerate(optim_paras["choices"]): + df[f"nonpecuniary_reward_{choice}"] = state_space.nonpec[indices][:, i] + df[f"wage_{choice}"] = state_space.wages[indices][:, i] + df[f"flow_utility_{choice}"] = flow_utilities[:, i] + df[f"value_function_{choice}"] = value_functions[:, i] + df[f"continuation_value_{choice}"] = continuation_values[cont_indices][:, i] - return rows + return df def _sample_characteristic(states_df, options, level_dict, use_keys): @@ -420,12 +396,12 @@ def _convert_codes_to_original_labels(df, optim_paras): """Convert codes in choice-related and observed variables to labels.""" code_to_choice = dict(enumerate(optim_paras["choices"])) - df.Choice = df.Choice.cat.set_categories(code_to_choice).cat.rename_categories( - code_to_choice - ) - for i in range(1, optim_paras["n_lagged_choices"] + 1): - df[f"Lagged_Choice_{i}"] = ( - df[f"Lagged_Choice_{i}"] + for choice_var in ["Choice"] + [ + f"Lagged_Choice_{i}" for i in range(1, optim_paras["n_lagged_choices"] + 1) + ]: + df[choice_var] = ( + df[choice_var] + .astype("category") .cat.set_categories(code_to_choice) .cat.rename_categories(code_to_choice) ) @@ -437,7 +413,7 @@ def _convert_codes_to_original_labels(df, optim_paras): return df -def _create_simulated_data(data, df, is_n_step_ahead, n_sim_p, optim_paras, options): +def _process_simulation_output(df, optim_paras): """Create simulated data. This function takes an array of simulated outcomes for each period and stacks them @@ -445,18 +421,9 @@ def _create_simulated_data(data, df, is_n_step_ahead, n_sim_p, optim_paras, opti Parameters ---------- - data : list - List of period-specific simulated outcomes. df : pandas.DataFrame - Original DataFrame passed by the user. - is_n_step_ahead : bool - Indicator for whether the simulation method is n-step-ahead or not. If it is - true, the individual identifier is generated. If false, take the identifier from - the data passed by the user. - n_sim_p : int - Number of periods for which outcomes are simulated. + DataFrame which contains the simulated data with internal codes and labels. optim_paras : dict - options : dict Returns ------- @@ -464,25 +431,15 @@ def _create_simulated_data(data, df, is_n_step_ahead, n_sim_p, optim_paras, opti DataFrame with simulated data. """ - if is_n_step_ahead: - identifier = np.tile(np.arange(options["simulation_agents"]), n_sim_p) - else: - identifier = df.index.get_level_values("identifier") - - col_dtype = generate_column_dtype_dict_for_simulation(optim_paras) - - simulated_df = ( - pd.DataFrame( - data=np.column_stack((identifier, np.row_stack(data))), columns=col_dtype - ) - .astype(col_dtype) - .sort_values(["Identifier", "Period"]) - .set_index(["Identifier", "Period"], drop=True) + df = df.rename(columns=rename_labels_from_internal).rename_axis( + index=rename_labels_from_internal ) + df = _convert_codes_to_original_labels(df, optim_paras) - simulated_df = _convert_codes_to_original_labels(simulated_df, optim_paras) + # We use the downcast to convert some variables to integers. + df = df.apply(downcast_to_smallest_dtype) - return simulated_df + return df def _random_choice(choices, probabilities, decimals=5): @@ -492,7 +449,7 @@ def _random_choice(choices, probabilities, decimals=5): The function is taken from this `StackOverflow post `_ as a workaround for - :func:`np.random.choice` as it can only handle one-dimensional probabilities. + :func:`numpy.random.choice` as it can only handle one-dimensional probabilities. Example ------- @@ -537,7 +494,7 @@ def _random_choice(choices, probabilities, decimals=5): return choices[indices] -def _apply_law_of_motion(df, choices, optim_paras): +def _apply_law_of_motion(df, optim_paras): """Apply the law of motion to get the states in the next period. For n-step-ahead simulations, the states of the next period are generated from the @@ -545,27 +502,25 @@ def _apply_law_of_motion(df, choices, optim_paras): previous choices according to the choice in the current period, to get the states of the next period. + We implicitly assume that observed variables are constant. + Parameters ---------- df : pandas.DataFrame - DataFrame with shape (n_individuals, n_state_space_dim) containing the state of - each individual. - choices : numpy.ndarray - Array with shape (n_individuals,) containing the current choice. + The DataFrame contains the simulated information of individuals in one period. optim_paras : dict Returns ------- df : pandas.DataFrame - DataFrame with containing the states of individuals to simulate outcomes for the - next period. + The DataFrame contains the states of individuals in the next period. """ n_lagged_choices = optim_paras["n_lagged_choices"] # Update work experiences. for i, choice in enumerate(optim_paras["choices_w_exp"]): - is_choice = choices == i + is_choice = df["choice"] == i df.loc[is_choice, f"exp_{choice}"] = df.loc[is_choice, f"exp_{choice}"] + 1 # Update lagged choices by deleting oldest lagged, renaming other lags and inserting @@ -585,45 +540,54 @@ def _apply_law_of_motion(df, choices, optim_paras): df = df.rename(columns=rename_lagged_choices) # Add current choice as new lag. - df.insert(position, "lagged_choice_1", choices) + df.insert(position, "lagged_choice_1", df["choice"]) # Increment period in MultiIndex by one. df.index = df.index.set_levels( df.index.get_level_values("period") + 1, level="period", verify_integrity=False ) + state_space_columns = create_state_space_columns(optim_paras) + df = df[state_space_columns] + return df -def _create_state_space_columns(optim_paras): - """Create names of state space dimensions excluding the period and identifier.""" - columns = ( - [f"exp_{choice}" for choice in optim_paras["choices_w_exp"]] - + [f"lagged_choice_{i}" for i in range(1, optim_paras["n_lagged_choices"] + 1)] - + list(optim_paras["observables"]) - ) +def create_core_state_space_columns(optim_paras): + """Create internal column names for the core state space.""" + return [f"exp_{choice}" for choice in optim_paras["choices_w_exp"]] + [ + f"lagged_choice_{i}" for i in range(1, optim_paras["n_lagged_choices"] + 1) + ] + + +def create_dense_state_space_columns(optim_paras): + """Create internal column names for the dense state space.""" + columns = list(optim_paras["observables"]) if optim_paras["n_types"] >= 2: columns += ["type"] return columns +def create_state_space_columns(optim_paras): + """Create names of state space dimensions excluding the period and identifier.""" + return create_core_state_space_columns( + optim_paras + ) + create_dense_state_space_columns(optim_paras) + + def _harmonize_simulation_arguments(method, df, n_sim_p, options): """Harmonize the arguments of the simulation.""" if method == "n_step_ahead_with_sampling": - df = None + pass else: if df is None: raise ValueError(f"Method '{method}' requires data.") options["simulation_agents"] = df.index.get_level_values("Identifier").nunique() - if method == "n_step_ahead_with_data": - df = df.query("Period == 0") - elif method == "one_step_ahead": + if method == "one_step_ahead": n_sim_p = int(df.index.get_level_values("Period").max() + 1) - else: - raise NotImplementedError(f"Method '{method}' is not implemented.") n_sim_p = options["n_periods"] if n_sim_p is None else n_sim_p if options["n_periods"] < n_sim_p: @@ -634,37 +598,60 @@ def _harmonize_simulation_arguments(method, df, n_sim_p, options): "model periods equal to simulated periods." ) - return df, n_sim_p, options + return n_sim_p, options -def _process_input_df_for_simulation(df, method, options, optim_paras): +def _process_input_df_for_simulation(df, method, n_sim_periods, options, optim_paras): """Process the ``df`` provided by the user for the simulation.""" - if df is None: + if method == "n_step_ahead_with_sampling": ids = np.arange(options["simulation_agents"]) - index = pd.MultiIndex.from_product((ids, [0]), names=["identifier", "period"]) + index = pd.MultiIndex.from_product( + (ids, range(n_sim_periods)), names=["identifier", "period"] + ) df = pd.DataFrame(index=index) + elif method == "n_step_ahead_with_data": + ids = np.arange(options["simulation_agents"]) + index = pd.MultiIndex.from_product( + (ids, range(n_sim_periods)), names=["identifier", "period"] + ) + df = ( + df.copy() + .rename(columns=rename_labels_to_internal) + .rename_axis(index=rename_labels_to_internal) + .query("period == 0") + .reindex(index=index) + .sort_index() + ) + + elif method == "one_step_ahead": + df = ( + df.copy() + .rename(columns=rename_labels_to_internal) + .rename_axis(index=rename_labels_to_internal) + .sort_index() + ) + else: - df = df.copy().rename(columns=rename_labels) - df = df.rename_axis(index=rename_labels).sort_index() + raise NotImplementedError - state_space_columns = _create_state_space_columns(optim_paras) + state_space_columns = create_state_space_columns(optim_paras) df = df.reindex(columns=state_space_columns) - first_period = df.query("period == 0") - has_nans = np.any(first_period.drop(columns="type", errors="ignore").isna()) - if has_nans and method != "n_step_ahead_with_sampling": + # Perform two checks for NaNs. + data = df.query("period == 0").drop(columns="type", errors="ignore") + has_nans_in_first_period = np.any(data.isna()) + if has_nans_in_first_period and method == "n_step_ahead_with_data": warnings.warn( "The data contains 'NaNs' in the first period which are replaced with " "characteristics implied by the initial conditions. Fix the data to silence" " the warning." ) - else: - pass - other_periods = df.query("period != 0") - has_nans = np.any(other_periods.drop(columns="type", errors="ignore").isna()) - if has_nans: - raise ValueError("The data must not contain NaNs beyond the first period.") + has_nans = np.any(df.drop(columns="type", errors="ignore").isna()) + if has_nans and method == "one_step_ahead": + raise ValueError( + "The data for one-step-ahead simulation must not contain NaNs." + ) return df diff --git a/respy/tests/test_data_checking.py b/respy/tests/test_data_checking.py deleted file mode 100644 index b92dac83a..000000000 --- a/respy/tests/test_data_checking.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - -import respy as rp -from respy.config import EXAMPLE_MODELS -from respy.pre_processing.data_checking import check_simulated_data -from respy.pre_processing.model_processing import process_params_and_options -from respy.tests.utils import process_model_or_seed - - -@pytest.mark.parametrize("model_or_seed", EXAMPLE_MODELS) -def test_simulated_data(model_or_seed): - """Test simulated data with ``check_simulated_data``. - - Note that, ``check_estimation_data`` is also tested in this function as these tests - focus on a subset of the data. - - """ - params, options = process_model_or_seed(model_or_seed) - - options["n_periods"] = 5 - - simulate = rp.get_simulate_func(params, options) - df = simulate(params) - - optim_paras, _ = process_params_and_options(params, options) - check_simulated_data(optim_paras, df) diff --git a/respy/tests/test_randomness.py b/respy/tests/test_randomness.py index 325f50dac..1e99cb29a 100644 --- a/respy/tests/test_randomness.py +++ b/respy/tests/test_randomness.py @@ -11,48 +11,19 @@ def test_invariance_of_model_solution_in_solve_and_criterion_functions(model_or_seed): params, options = process_model_or_seed(model_or_seed) - options["n_periods"] = 3 if options["n_periods"] > 3 else options["n_periods"] + options["n_periods"] = 2 if model_or_seed == "kw_2000" else 3 state_space = rp.solve(params, options) - simulate = rp.get_simulate_func(params, options) - _ = simulate(params) - state_space_ = simulate.keywords["state_space"] - - np.testing.assert_array_equal(state_space.states, state_space_.states) - np.testing.assert_array_equal(state_space.wages, state_space_.wages) - np.testing.assert_array_equal(state_space.nonpec, state_space_.nonpec) - np.testing.assert_array_equal( - state_space.emax_value_functions, state_space_.emax_value_functions - ) - np.testing.assert_array_equal( - state_space.base_draws_sol, state_space_.base_draws_sol - ) - - -@pytest.mark.parametrize("model_or_seed", EXAMPLE_MODELS + list(range(5))) -def test_invariance_of_model_solution_in_solve_and_crit_func(model_or_seed): - params, options = process_model_or_seed(model_or_seed) - options["n_periods"] = 3 if options["n_periods"] > 3 else options["n_periods"] - - state_space = rp.solve(params, options) - - # Simulate data. simulate = rp.get_simulate_func(params, options) df = simulate(params) + state_space_sim = simulate.keywords["state_space"] - criterion_functions = [ - simulate, - rp.get_crit_func(params, options, df), - ] - - for crit_func in criterion_functions: - _ = crit_func(params) - if "state_space" in crit_func.keywords: - state_space_ = crit_func.keywords["state_space"] - else: - state_space_ = crit_func.keywords["simulate"].keywords["state_space"] + criterion = rp.get_crit_func(params, options, df) + _ = criterion(params) + state_space_crit = criterion.keywords["state_space"] + for state_space_ in [state_space_sim, state_space_crit]: np.testing.assert_array_equal(state_space.states, state_space_.states) np.testing.assert_array_equal(state_space.wages, state_space_.wages) np.testing.assert_array_equal(state_space.nonpec, state_space_.nonpec) diff --git a/respy/tests/test_replication_kw_94.py b/respy/tests/test_replication_kw_94.py index eb1d487a1..956a422a2 100644 --- a/respy/tests/test_replication_kw_94.py +++ b/respy/tests/test_replication_kw_94.py @@ -31,6 +31,7 @@ from respy.config import TEST_RESOURCES_DIR +@pytest.mark.slow def test_table_6_exact_solution_row_mean_and_sd(): """Replicate the first two rows of Table 6 in Keane and Wolpin (1994). diff --git a/respy/tests/test_simulate.py b/respy/tests/test_simulate.py index 87d19250c..2603c273d 100644 --- a/respy/tests/test_simulate.py +++ b/respy/tests/test_simulate.py @@ -4,9 +4,40 @@ import pytest import respy as rp +from respy.config import EXAMPLE_MODELS from respy.likelihood import get_crit_func +from respy.pre_processing.data_checking import check_simulated_data +from respy.pre_processing.model_processing import process_params_and_options from respy.pre_processing.specification_helpers import generate_obs_labels from respy.tests.random_model import generate_random_model +from respy.tests.utils import process_model_or_seed + + +@pytest.mark.parametrize("model_or_seed", EXAMPLE_MODELS) +def test_simulated_data(model_or_seed): + """Test simulated data with ``check_simulated_data``. + + Note that, ``check_estimation_data`` is also tested in this function as these tests + focus on a subset of the data. + + """ + params, options = process_model_or_seed(model_or_seed) + + options["n_periods"] = 5 + + simulate = rp.get_simulate_func(params, options) + df = simulate(params) + + optim_paras, _ = process_params_and_options(params, options) + check_simulated_data(optim_paras, df) + + +@pytest.mark.skip +def test_one_step_ahead_simulation(): + params, options, df = rp.get_example_model("kw_97_basic") + options["n_periods"] = 11 + simulate = rp.get_simulate_func(params, options, "one_step_ahead", df) + df = simulate(params) @pytest.mark.parametrize("seed", range(20)) @@ -31,7 +62,12 @@ def test_equality_for_myopic_agents_and_tiny_delta(seed): crit_func_ = rp.get_crit_func(params, options, df_) likelihood_ = crit_func_(params) - pd.testing.assert_frame_equal(df, df_) + # The continuation values are different because for delta = 0 the backward induction + # is completely skipped and all continuation values are set to zero whereas for a + # tiny delta, the delta ensures that continuation have no impact. + columns = df.filter(like="Continu").columns.tolist() + pd.testing.assert_frame_equal(df.drop(columns=columns), df_.drop(columns=columns)) + np.testing.assert_almost_equal(likelihood, likelihood_, decimal=12)